# plotmaker.rb: The main class for making plots
# copyright (c) 2006, 2007, 2008 by Vincent Fourmond
  
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
  
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details (in the COPYING file).


# TODO, the main one:
# 
# It currently is a pain to make complex plots with ctioga. A real
# pain. What could be done to improve the situation ?
# 
# * hide the difference between edges and axes.
# * the layout mechanism is not comfortable enough to work with, especially
#   with the need for relative positioning.
#
# Would it be possible to allow for the 'real size' to be determined
# *afterwards* ? 

# TODO, an even bigger one:
# Switch to a real command-based plotting program:
#  - any single operation that is realized by ctioga would be a command
#  - every single of these commands would take a given (fixed) number of
#    parameters (we should take care about boolean stuff)
#  - every command would be of course reachable as command-line options
#    but it could also be within files
#  - in these files, provide an additional mechanism for quickly defining
#    variables and do variable substitution.
#  - one command (plus arguments) per line, with provisions for
#    line-splitting
#  - allow some kind of 'include' directives (that would also be used for
#    cmdline inclusion of files)
#  - command-line arguments and command files could intermix (that *would*
#    be fun, since it would allow very little changes to a command-line
#    to change significantly the look of a file...!)
#  - command files would be specified using @file ?
#  - in the absence of --name commands, output would go to the named file ?
#  - LONG TERM: allow conditionals and variable
#    definition/substitution on command-line ?
#
#  Each command could take *typed* arguments. That would allow typed
#  variables along with a string-to-type conversion ? (is that useful ?)
#
#  Provide *optional* hash-like arguments that probably could not be used
#  in the command-line, but could be in the file.
#
#  Provide self-documentation in each and every command
#
#  Manipulations of a buffer stack - including mathematical
#  expressions; provide commands to only *load* a file, but not
#  necessarily import it.
#
#  Provide a way to 'save' a command-line into a command-file.
#
#  Write as many test suites as possible ??
#
#  Merge Metabuilder and Backends into the ctioga code base. There's
#  no need for extra complexity.
#
#  That requires a huge amount of work, but on the other hand, that
#  would be much more satisfactory than the current mess.
#
#  Commands would be part of "groups".
#
#  Release a new version of ctioga before that.
#
#  Don't rely on huge mess of things !
#
#  Then, I could have some small fun and write a 



# Very important for command-line parsing
require 'optparse'
require 'Tioga/tioga'
require 'Tioga/Utils'

# Information about style
require 'CTioga/styles'
# Moving arrays
# require 'CTioga/movingarrays'
# And, most important of all, elements
require 'CTioga/elements'

# The debugging facility
require 'CTioga/debug'
require 'CTioga/log'
require 'CTioga/utils'
require 'CTioga/layout'


# The backends
require 'CTioga/backends'

# Support for themes
require 'CTioga/themes'
require 'CTioga/axes'
require 'CTioga/structures'

# Legends
require 'CTioga/legends'


# for interpreting the CTIOGA environment variable
require 'shellwords'
require 'MetaBuilder/metabuilder'


module CTioga

  Version::register_svn_info('$Revision: 882 $', '$Date: 2009-02-24 15:23:12 +0100 (Tue, 24 Feb 2009) $')



  # The regular expression saying that what we see on the command-line
  # means "use default value".
  DEFAULT_RE = /^\s*(auto|default)\s*$/i
  
  # The regular expression saying that what we see on the command-line
  # means "disable".
  DISABLE_RE = /^\s*(no(ne)?|off)\s*$/i

  # The regular expression saying that what we see on the command-line
  # means "true".
  TRUE_RE = /^\s*(true|yes|on)\s*$/i

  # A small function to help quoting a string for inclusion
  # in a pdfTeX primitive.
  def self.pdftex_quote_string(str)
    return str.gsub(/([%#])|([()])|([{}~_^])|\\/) do 
      if $1
        "\\#{$1}"
      elsif $2                  # Quoting (), as they can be quite nasty !!
        "\\string\\#{$2}"
      elsif $3
        "\\string#{$3}"
      else                      # Quoting \
        "\\string\\\\"
      end
    end
  end


  # This class is responsible for reading the command-line, via it's parse
  # function, and to actually turn it into nice Tioga commands to make
  # even nicer graphes.
  class PlotMaker

    # PlotMaker is now handled (at least partially) by MetaBuilder.
    # That should save a lot of code.

    include MetaBuilder::DescriptionInclude
    extend  MetaBuilder::DescriptionExtend
    
    describe 'test', "A test class", <<EOD
A class to test visually the effects of different stuff
EOD

    include SciYAG
    include Tioga

    include Debug
    include Log
    include Utils               # For the safe_float function

    # For dimension conversion and TeX quoting.
    include Tioga::Utils

    # For the backend handling:
    include CTioga::Backends

    # Support for Themes:
    include Themes

    # Axes, edges, labels, tick labels:
    include Axes


    # these are basically the attributes which are modified directly
    # on the command-line
    attr_writer :fig_name, :cleanup, :legend

    # The command-line parser
    attr_reader :parser

    # The last Curve object on the stack
    attr_reader :last_curve

    # Whether we are trying to do real-size PDF or not. When set, it
    # is the size of the PDF
    attr_accessor :real_size

    # The current object
    attr_accessor :current_object

    # Whether we are making a PNG file
    attr_accessor :png

    # The PNG's size:
    attr_accessor :png_size

    # Whether to create separate legends
    attr_accessor :separate_legends
    
    # Whether to automatically add a legend to all curves
    attr_accessor :autolegends

    def initialize
      # The first thing to do is to setup logging, as you really
      # want to be able to communicate with users, don't you ?
      init_logger


      @args = []                # Holding the command-line
      @parser = OptionParser.new

      initialize_themes

      # Whether the plots should be interpolated.
      @interpolate = false 

      @line_width = nil

      @fig_name = "Plot"

      # Initialize the backend structure:
      init_backend_structure

      init_axes

      # now, the elements for the structure of the plot:
      @root_object = SubPlot.new
      # We start with a very simple layout
      SimpleLayout.new(@root_object)
      
      # the current object is the one to which we'll add plot elements.
      @current_object = @root_object

      # @legend is holding the text attached to the current item being
      # plotted; it has to be cleared manually after every item processed
      @legend = nil

      # general purpose variables:
      @cleanup = true           # Now, cleaning is done by default !
      @viewer = false

      # Some options we could set on the command line
      @init_funcalls = []

      # Whether to provide default legends. On by default
      @autolegends = true

      @command_line = ""

      @real_size = "12cmx12cm"

      # If set, should be the size of the TeX font
      
      @tex_fontsize = nil

      # The frame sides for the setup_real_size function
      @frame_sides = [0.1,0.9,0.9,0.1]
      # Override this, as the layout scheme makes it more or less
      # obsolete
      @frame_sides = [0,1,1,0]

      # If set, we create separate legend informations:
      @separate_legend = false

      # The array holding the styles.
      @separate_legend_styles = []
      # The size of the produced PDF in PDF points.
      @separate_legend_width = 12
      @separate_legend_height = 8

      # Whether to mark the command-line in the PDF file
      @mark = true              # On by default, really useful !!!

      # The LaTeX preamble:
      @preamble = ""

      # The last curve used:
      @last_curve = nil

      # Specifications for the --next stuff: an array, the first
      # element is the class to be created for children and the
      # rest are arguments to be added at the beginning.
      @next_specs = [SubPlot, :subplot]

      # A block to be run on both the old and new object
      # when --next is encountered.
      @next_block = nil

      # Whether we automatically start a --next stuff on each spec
      # or even on each dataset
      @auto_next = false

      # We don't make any PNG file:
      @png = false

      # The standard PNG density...
      @png_oversampling = 2

      # We don't produce SVG output by default
      @svg = false

      # The default padding;
      @default_padding = []
      4.times do 
        @default_padding << Dimension.new(0.05)
      end

      # And we use it :
      use_default_padding
      
      prepare_option_parser
    end

    # Sets the given's object padding to the current default:
    def use_default_padding(obj = nil)
      obj = current_object unless obj
      4.times do |i|
        obj.layout_preferences.padding[i] = @default_padding[i].dup
      end
      debug "Setting padding for #{identify(obj)} to " +
        obj.layout_preferences.padding.inspect
    end


    # Adds the given object to the current's stack and set it
    # as the current object, so that it will receive further
    # children
    def enter_child_object(object)
      @current_object.add_elem(object)
      self.current_object = object
    end

    # Goes out from a child object to its parent. Returns the child
    # object. Issues a warning if already at top level (in which case
    # it returns the current object)
    def leave_child_object
      prev = current_object
      if current_object.parent
        self.current_object = current_object.parent
      else
        warn "--end while in top level"
      end
      return prev
    end


    # Returns the current object's plot style
    def current_plot_style
      return current_object.plot_style
    end



    # This function reads a configuration file and executes it's
    # statements in the scope of a local module, which is then
    # included in this instance, and in the backend's instances.
    def read_config_file(file)
      f = File.open(file)
      info "Reading config file #{file}"
      lines = f.readlines
      lines << "\nend\n"
      lines.unshift "module CTiogaRC\n"
      code = lines.join
      eval code
      extend CTiogaRC
      for b_e in backends.values
        b_e.extend CTiogaRC
      end
      # This is for the compute_formula function.
      Dvector.extend CTiogaRC
    end
    
    CONFIG_FILE_NAME = ".ctiogarc"

    # Looks for a configuration file, either in the current directory
    # or in the HOME directory.
    def lookup_config_file
      if File.readable? CONFIG_FILE_NAME
        return CONFIG_FILE_NAME
      end

      if ENV.has_key?('HOME')
        home_rc = File.join(ENV['HOME'], CONFIG_FILE_NAME)
        if File.readable?(home_rc)
          return home_rc
        end
      end
      return nil
    end

    # Sets the frame of the root object. This is absolutely
    # necessary for layout computation. Failure to do so will
    # result in failed plots.
    def set_root_frame(*frames)
        @root_object.root_frame = frames
    end

    # sets up the FigureMaker object to use for real size. Uses the
    # @real_size instance variable as a source for the size
    def setup_real_size(t)
      # Get the width and height from @real_size
      sizes = @real_size.split("x").collect {|s| 
        tex_dimension_to_bp(s)
      }

      t.def_enter_page_function {
        t.page_setup(*sizes)
        t.set_frame_sides(*@frame_sides) 
        set_root_frame(sizes[0] * @frame_sides[0], 
                       sizes[0] * @frame_sides[1],
                       sizes[1] * @frame_sides[2], 
                       sizes[1] * @frame_sides[3])
      }

      # Setting label and title scale to 1
      t.title_scale = 1
      t.xlabel_scale = 1
      t.ylabel_scale = 1
    end

    # Reads a configuration file if one is found.
    def read_config
      if lookup_config_file
        read_config_file(lookup_config_file)
      end
    end


    # Push a function call onto the stack of the current element
    def add_elem_funcall(sym, *args)
      @current_object.add_funcall(TiogaFuncall.new(sym, *args))
    end

    # Push a function call onto the stack of things we should do
    # just after creating the FigureMakerobject
    def add_init_funcall(sym, *args)
      @init_funcalls << TiogaFuncall.new(sym, *args)
    end

    # Forwards the boundary settings to the current object.
    def set_bounds(which, val)
      method = ("bound_#{which}=").to_sym
      @current_object.send(method, val)
    end

    def set_range(a,b,str)
      first,last = str.split(/\s*:\s*/)
      first = first.to_f if first
      last = last.to_f if last
      set_bounds(a,first)
      set_bounds(b,last)
    end

    # Splits a text into four components, expanding if necessary:
    # * if there is only one element, all four become this one
    # * if there are two: w,h, it becomes w,w,h,h
    # * if there are three, the last element is duplicated.
    def expand_sides(txt)
      ary = txt.split(/\s*,\s*/)
      case ary.size
      when 1
        return [ary[0], ary[0], ary[0], ary[0]]
      when 2
        return [ary[0], ary[0], ary[1], ary[1]]
      when 3
        return [ary[0], ary[1], ary[2], ary[2]]
      else
        return ary
      end
    end

    def margins_from_text(txt)
      ary = expand_sides(txt).map {|s| s.to_f}
      return ary
    end

    # In the order: left, right, bottom, top, to suit Tioga's
    # default positioning of the text along the axis.
    def set_frame_margins(val)
      @frame_sides = [val[0], 1.0 - val[1], 1.0 - val[3], val[2]]
    end

    # This function prepares the parser by giving it the arguments and
    # some small help text going along with them. This function will
    # unfortunately get really large with time, but there's nothing
    # to do about it.
    def prepare_option_parser
      theme_prepare_parser(@parser)

      @parser.separator "\nLaTeX options"
      @parser.on("--use PACKAGE",
                 "Adds PACKAGE to the LaTeX preamble") do |w|
        @preamble += "\\usepackage{#{w}}\n"
      end

      @parser.on("--preamble STRING",
                 "Adds STRING to the LaTeX preamble") do |w|
        @preamble += "#{w}\n"
      end

      @parser.separator "\nGlobal look:"

      @parser.on("--[no-]background [COLOR]",
                 "Sets the background color for plots") do |c|
        # Keep it to false if only false
        c = CTioga.get_tioga_color(c) if c
        current_plot_style.background_color = c
      end

      @parser.on("--[no-]watermark [TEXT]",
                 "Writes a text as a watermark at the back ", 
                 "of the plot") do |c|
        current_plot_style.watermark_text = c
      end

      @parser.on("--watermark-color [COLOR]",
                 "Chooses the color of the watermark") do |c|
        c = CTioga.get_tioga_color(c) if c
        current_plot_style.watermark_color = c
      end

      @parser.on("--aspect-ratio [RATIO]",
                 "Sets the aspect ratio (defaults to 1)") do |a|
        a = 1.0 unless a
        a = a.to_f
        if a <= 0.0
          warn "Aspect ratio #{a} not valid, ignored" 
        else
          add_elem_funcall(:set_aspect_ratio, a)
        end
      end
      @parser.on("--golden-ratio",
                 "Sets the aspect ratio to the golden number") do |a|
        add_elem_funcall(:set_aspect_ratio, 1.61803398874989)
      end
      @parser.on("--xrange RANGE",
                 "X plotting range") do |a|
        set_range('left','right', a)
      end
      @parser.on("--yrange RANGE",
                 "y plotting range") do |a|
        set_range('bottom','top', a)
      end
      @parser.on("--margin MARGIN",
                 "Sets the margin around data", "(left,right,top,bottom",
                 "in fractions of the corresponding size)") do |v|
        current_object.plot_margins = margins_from_text(v)
      end

      @parser.on("--rescale FACTOR",
                 "Scales everything by a given factor",
                 "(useful for subplots)") do |fact|
        current_object.rescale = safe_float(fact)
      end


      # TODO: this should probably move to PlotStyle
      @parser.on("--padding PADDING",
                 "Changes the padding for the current object",
                 "and subsequent ones") do |pad|
        ary = expand_sides(pad)
        @default_padding = ary.map {|dim| Dimension.new(dim)}
        use_default_padding
      end

      axes_options(@parser)

      @parser.separator "\nSubfigures, subplots, regions..."

      @parser.on("--y2",
                 "Switch to an alternative Y axis") do
        begin
          current_object.disable_axis(:y)
        rescue
          # Purely ignore errors
          warn "Something should have happened here "+
            "that did not happen properly"
        end
        plot = SharedAxisPlot.new(:y, current_object)
        # The label will be on the right
        plot.plot_style.ylabel.side = RIGHT
        plot.plot_style.edges.yaxis.loc = RIGHT
        plot.plot_style.edges.set_edges(AXIS_HIDDEN, :left,:top,:bottom)
        current_plot_style.edges.set_edges(AXIS_LINE_ONLY, :right)
        # A simple layout will control the plot
        layout = FixedLayout.new(plot)        
        # And this layout should report its info to
        # the current one.
        #
        # Note that we add the child's *layout* to the
        # objects layout, not the child itself.
        #
        # Won't fail if the current plot does not have a layout
        if current_object.layout
          current_object.layout.add_child(layout)
        end
        enter_child_object(plot)
      end

      @parser.on("--x2",
                 "Switch to an alternative X axis") do
        begin
          current_object.disable_axis(:y)
        rescue
          # Purely ignore errors
          warn "Something should have happened here " +
            "that did not happen properly"
        end
        plot = SharedAxisPlot.new(:x, current_object)
        # The label will be on the right
        plot.xlabel.side = TOP
        plot.edges.xaxis.loc = TOP
        plot.edges.set_edges(AXIS_HIDDEN, :left,:right,:bottom)
        current_object.edges.set_edges(AXIS_LINE_ONLY, :top)
        layout = FixedLayout.new(plot)        
        # And this layout should report its info to
        # the current one.
        #
        # Note that we add the child's *layout* to the
        # objects layout, not the child itself.
        current_object.layout.add_child(layout)
        enter_child_object(plot)
      end

      @parser.on("--inset SPEC",
                 "Creates an inset with the given",
                 "specifications (x,y:w[xh] or x1,y1;x2,y2)" ) do |a|
        plot = SubPlot.new(:subplot, current_object)
        plot.show_legend = true
        enter_child_object(plot)
        # TODO - high priority !
        # implement a way to specify insets with *real* tex dimensions
        # !!!! (such as 3cm,2cm:5cm) !
        #
        # This would *really* make way for real-size graphs
        margins = Utils::inset_margins(a)
        debug "inset margins: #{margins.inspect}"
        current_object.convert_layout(FixedLayout)
        current_object.layout.position = margins
        # By default, we redirect all legends for the inset
        # to the parent.
        current_object.accept_legend = false
      end

      @parser.on("--zoom-inset SPEC",
                 "Creates an inset as with --inset " +
                 "and copies all the ",
                 "elements in the current plot there") do |a|
        plot = SubPlot.new(:subplot, current_object)
        # We redirect all legends for the inset to the parent.
        plot.accept_legend = false
        # Temporarily disable legends.
        plot.disable_legend = true
        for obj in @current_object.elements
          plot.add_elem(obj.dup)
        end
        for obj in @current_object.funcalls
          plot.add_funcall(obj.dup)
        end
        plot.disable_legend = false
        # Better add it afterwards...
        enter_child_object(plot)
        margins = Utils::inset_margins(a)
        debug "zoom margins: #{margins.inspect}"
        current_object.convert_layout(FixedLayout)
        current_object.layout.position = margins
      end

      @parser.on("--next-inset SPEC",
                 "Equivalent to --end --inset SPEC, except that",
                 "various style information are carried from",
                 "the previous inset") do |a|
        plot = SubPlot.new(:subplot, current_object)
        plot.show_legend = true
        old = leave_child_object
        enter_child_object(plot)

        # Force copy of the style:
        plot.plot_style = old.plot_style.deep_copy(plot)

        margins = Utils::inset_margins(a)
        debug "inset margins: #{margins.inspect}"
        current_object.convert_layout(FixedLayout)
        current_object.layout.position = margins
        # We redirect all legends for the inset to the parent.
        current_object.accept_legend = false
      end

      @parser.on("--subplot SPEC",
                 "Creates a subplot with the given specifications",
                 "(x,y:w[xh] or x1,y1;x2,y2)" ) do |a|
        plot = SubPlot.new(:subplot, current_object)
        enter_child_object(plot)
        margins = Utils::inset_margins(a)

        debug "subplot margins: #{margins.inspect}"
        # In contrast with --inset, we use a layout:
        current_object.layout = SimpleLayout.new(current_object)
        current_object.layout.bounding_box = margins

        # We do not redirect legends for a subplot
        current_object.accept_legend = true

      end

      @parser.on("--next-subplot SPEC",
                 "Creates a subplot with the given specifications",
                 "(x,y:w[xh] or x1,y1;x2,y2)" ) do |a|
        plot = SubPlot.new(:subplot, current_object)
        old = leave_child_object
        enter_child_object(plot)
        margins = Utils::inset_margins(a)

        # Force copy of the style:
        plot.plot_style = old.plot_style.deep_copy(plot)

        debug "subplot margins: #{margins.inspect}"
        # In contrast with --inset, we use a layout:
        current_object.layout = SimpleLayout.new(current_object)
        current_object.layout.bounding_box = margins

        # We do not redirect legends for a subplot
        current_object.accept_legend = true

      end



      @parser.on("--disable-legends",
                 "Disable the display of legends for the current " +
                 "subplot/subfigure") do
        current_object.disable_legend = true
      end

      @parser.on("--enable-legends",
                 "Reverts --disable-legends") do
        current_object.disable_legend = false
      end

      @parser.on("--[no-]forward-legends",
                 "Forwards the legends to the parent object") do |v|
        current_object.accept_legend = ! v
      end

      # Black magic is starting here !
      @parser.on("--grid SPEC",
                 "Creates a grid. SPEC is column|row=nb") do |s|
        # get the spec:
        s =~ /(\w+)=(\d+)/
        which = "#{$1}s=".to_sym
        number = $2.to_i
        if which == :columns= or which == :rows=
          current_object.layout = current_object.layout.
            convert_layout(GridLayout)

          current_plot_style.hide_axis_and_edges(:x, :y)

          # We rescale the padding by a factor of 1/number, so it looks
          # reasonable in the end.
          @default_padding.each do |dim|
            dim.scale!(1.0/number)
          end

          # We use the default padding for the chidren
          use_default_padding

          current_plot_style.set_xy_labels(nil, nil)

          current_object.layout.send(which, number)
          plot = SubPlot.new(:subplot, current_object)
          plot.accept_legend = false
          @current_object.layout.add_child(GridItemLayout.new(plot))
          @current_object.add_elem(plot)
          self.current_object = plot
          @next_spec = [SubPlot, :subplot]
          @next_block = proc do |old, new|
            new.accept_legend = false # By default, forward to the parent.
            use_default_padding(new)
          end
        else
          warn "Unrecognized spec #$1"
        end
      end

      # Black magic is starting here !
      @parser.on("--col",
                 "Starts a column of graphes with shared X axis") do
        # We convert the current object to a GridLayout. If
        # there are already plots, that really won't look good.
        # You've been warned !!!
        current_object.layout = current_object.layout.
          convert_layout(GridLayout)
#        current_object.edges.disable

        current_plot_style.hide_axis_and_edges(:x, :y)

        current_object.layout_preferences.padding[2] = Dimension.new(0.0)
        current_object.layout_preferences.padding[3] = Dimension.new(0.0)

        current_object.layout.columns = 1
        plot = SharedAxisPlot.new(:y, current_object)
        @next_spec = [SharedAxisPlot, :y]
        @current_object.layout.add_child(GridItemLayout.new(plot))
        @current_object.add_elem(plot)
        
        # We copy the X label and the title
        plot.plot_style.xlabel.label = current_plot_style.xlabel.label
        current_plot_style.xlabel.label = nil
        plot.plot_style.title.label = current_plot_style.title.label
        current_plot_style.title.label = nil
        self.current_object = plot

        # We cancel vertical padding by default:
        @default_padding[2] = Dimension.new(0.0)
        @default_padding[3] = Dimension.new(0.0)

        use_default_padding(plot)
        # Forwards the x label to the child, and cancels it here.
        # The code to be run on the old and new tings
        @next_block = proc do |old, new|
          old.plot_style.edges.
            set_axis_and_edges_style(:x, AXIS_WITH_TICKS_ONLY)
          old.plot_style.xlabel.label = nil

          # Disable title by default
          new.plot_style.title.label = nil
          use_default_padding(new)
        end
      end

      @parser.on("--next",
                 "Start the next object") do
        next_object
      end

      @parser.on("--[no-]auto-next",
                 "Automatically start a --next graph",
                 "for every element on the command-line") do |v|
        if v
          @auto_next = :element
        else
          @auto_next = false
        end
      end

      @parser.on("--auto-next-expanded",
                 "Automatically start a --next graph",
                 "for every element on the command-line",
                 "after data set expansion !") do
        @auto_next = :dataset
      end


#       @parser.on("--subframes FRAMES",
#                  "Setup the frames relative to the parent", 
#                  "for the current " +
#                  "subplot",
#                  "(distance from left,right,top,bottom)") do |v|
#         current_object.frame_margins = margins_from_text(v)
#       end

      @parser.on("--region",
                 "Start a colored region") do
        plot = Region.new(current_object)
        @current_object.add_elem(plot)
        self.current_object = plot
      end

      @parser.on("--region-color COLOR",
                 "The color of the region") do |v|
        c = CTioga.get_tioga_color(v)
        begin
          current_object.region_color = c
        rescue
          error "The current container isn't a --region !"
        end
      end

      @parser.on("--region-transparency TRANS",
                 "The transparency of the region") do |v|
        c = v.to_f
        begin
          current_object.region_transparency = c
        rescue
          error "The current container isn't a --region !"
        end
      end

      @parser.on("--region-dont-display",
                 "No curve until the next --end will actually be ",
                 "displayed: they are just used to delimit the ",
                 "region used for the fills") do
        begin
          current_object.dont_display = true
        rescue
          error "The current container isn't a --region !"
        end
      end

      @parser.on("--region-debug",
                 "Setup the fills for the curves inside the",
                 "region to help understanding what happens") do
        begin
          current_object.region_debug = true
        rescue
          error "The current container isn't a --region !"
        end
      end

      @parser.on("--region-invert-rule",
                 "Inverts the rule for choosing the filled region") do
        begin
          current_object.invert_rule = true
        rescue
          error "The current container isn't a --region !"
        end
      end

      @parser.on("--region-fill-twice",
                 "Fills the region twice, choosing opposite",
                 "rules for the boundaries.") do
        begin
          current_object.fill_twice = true
        rescue
          error "The current container isn't a --region !"
        end
      end

      @parser.on("--end",
                 "Ends the last subplot or region") do 
        leave_child_object
      end
      

      @parser.separator "\nOptions for real size output:"
      # Real size
      @parser.on("-r", "--[no-]real-size [SIZE]",
                 "Tries to produce a PDF file suitable",
                 "for inclusion " +
                 "with \\includegraphics") do |spec|
        @real_size = spec
      end
      @parser.on("--frame-margins VAL",
                 "The proportion of space to leave on", 
                 "each side for the text display" ) do |val|
        set_frame_margins(margins_from_text(val))
      end

      Legends::fill_option_parser(@parser, self)

      @parser.separator "\nTexts:"

      ## Default TeX fontsize
      @parser.on("--fontsize NB",
                 "Default TeX fontsize in points") do |size|
        @tex_fontsize = size
      end

      @parser.on("--tick-label-scale SCALE",
                 "Sets the scale of the text for tick labels") do |l|
        add_elem_funcall(:xaxis_numeric_label_scale=,Float(l))
        add_elem_funcall(:yaxis_numeric_label_scale=,Float(l))
      end


      @parser.on("--[no-]separate-legends",
                 "If set, PDF files are produced that contain",
                 "what the legend pictogram would look like") do |l|
        @separate_legends = l
        if l
          # Switches off autolegends by default
          @autolegends = false
        end
      end

      @parser.separator "\nGraphic primitives:"
      @parser.on("-d","--draw SPEC",
                 "Draw a graphic primitive") do |spec|
        add_elems_to_current(*TiogaPrimitiveMaker.parse_spec(spec, self))
      end
      @parser.on("--draw-help",
                 "Display what is known about graphic", 
                 "primitives and exit") do
        puts
        puts "Currently known graphics primitives are"
        puts
        puts TiogaPrimitiveMaker.introspect
        exit
      end

      prepare_backend_options(@parser)
                                              
      @parser.separator "\nHousekeeping and miscellaneous options:"
      @parser.on("--xpdf", 
                 "Runs xpdf to show the plot obtained") do 
        @viewer = "xpdf -z page" # With the zoom set to full page
      end

      @parser.on("--open", 
                 "Runs open to show the plot obtained") do 
        @viewer = "open"
      end

      @parser.on("--[no-]viewer [VIEWER]", 
                 "Runs VIEWER to show the plot obtained --",
                 "or doesn't show up the viewer at all") do |x|
        @viewer = x
      end
      
      @parser.on("--[no-]cleanup", 
                 "Removes all the accessory files produced") do |x|
        @cleanup = x
      end

      @parser.on("--tex-cleanup", 
                 "Removes all files produced except those",
                 "necessary " +
                 "for inclusion in a TeX document") do 
        @tex_cleanup = true
      end

      @parser.on("--clean-all", 
                 "Removes all files produced -- better use",
                 "along with " +
                 "--xpdf or --open") do 
        @clean_all = true
      end
      
      @parser.on("--save-dir DIR", 
                 "Sets the directory for output") do |x|
        add_init_funcall(:save_dir=, x)
      end

      
      @parser.on("--include FILE",
                 "Imports the file's functions into ctioga's",
                 "namespace so that they can be used", 
                 "by backends") do |file|
        read_config_file(file)
      end

      @parser.on("-n", "--name NAME",
                 "Base name") do |name|
        @fig_name = name
      end

      @parser.on("-o", "--output NAME",
                 "Produces a graph named NAME with the current output") do |name|
        output_figure(name)
      end

      @parser.on("--echo", "Prints command-line to standard output") do
        puts "Command-line used:"
        puts @command_line
      end

      @parser.on("--display-commandline",
                 "Adds the command line used to make",
                 "to graph on the bottom of the graph") do 
        add_elem_funcall(:show_marker, 
                         {
                           "x" => 0.5,
                           "y" => -0.2,
                           "text" => @command_line,
                           "scale" => 0.6,
                           "font" => Tioga::MarkerConstants::Courier,
                         }
                         )
      end

      @parser.on("--[no-]mark", 
                 "Fills the 'creator' field of the " ,
                 "produced PDF file with the command-line",
                 "when on. ") do |v|
        @mark = v
      end

      logger_options(@parser)
      @parser.on("--debug-patterns", 
                 "Produces an alignement-checking PDF file") do |v|
        @debug_patterns = v
      end

      @parser.on("--eps", 
                 "Attempt to produce an EPS file,",
                 "using pdftops, LaTeX and dvips.",
                 "The PDF output is kept in any case") do |v|
        @eps = v
      end

      @parser.on("--png SIZE", 
                 "Runs ctioga as usual, and runs convert afterwards",
                 "to produce a nice PNG file") do |s|
        @png = true
        s =~ /(\d+)x(\d+)/
        @png_size = [$1,$2]
        # We setup the real size so that it matches the number of
        # postscript points:
        @real_size = "#{$1}bpx#{$2}bp"
      end

      @parser.on("--png-oversampling NB", 
                 "How many more points to produce before downscaling") do |s|
        @png_oversampling = s.to_f
      end

      @parser.on("--svg", 
                 "Runs ctioga as usual, and runs pdf2svg afterwards",
                 "to produce a nice SVG file") do |s|
        @svg = true
      end


      @parser.on("--args FILE",
                 "Reads argument from FILE, *one per",
                 "line* and then resumes normal processing") do |v|
        File.open(v) do |f|
          unshift_cmdline_args(*f.readlines.collect {|s| s.chomp})
        end
      end

      @parser.on_tail("-h", "--help", "Show this message") do
        puts banner
        puts 
        puts @parser
        puts 
        puts "Please note that all the options can be abbreviated"
        puts "as long as they are still unique"
        exit
      end

      @parser.on_tail("--version", "Show version number") do
        puts "This is ctioga version " + Version::version
        exit
      end
    end


    # Start the next object in a complex plot (--grid, --col...)
    def next_object
      # We swicth back to the parent object
      old = leave_child_object
      a = @next_spec.dup      # We need to dup, else shift would make the
      # next curve not that happy...
      cls = a.shift
      # We push the parent object as the last element for new.
      a << current_object
      plot = cls.new(*a)
      @current_object.layout.add_child(GridItemLayout.new(plot))
      enter_child_object(plot)

      # We copy the style from the old plot to the new one:
      plot.plot_style = old.plot_style.deep_copy(plot)
        
      @next_block.call(old, plot) if @next_block
    end


    def unshift_cmdline_args(*args)
      @args.unshift(*args)
    end
    
    # the functions for plot structure...
    def subplot
      new_object = SubPlot.new(:subplot,@current_object)
      @current_object.add_elem(new_object)
      @current_object = new_object
    end

    def subfigure
      new_object = SubPlot.new(:subfigure,@current_object)
      @current_object.add_elem(new_object)
      @current_object = new_object
    end

    def end
      @current_object = @current_object.parent if @current_object.parent
    end

    def add_elems_to_current(*elems)
      for el in elems
        @current_object.add_elem(el)
      end
    end

    # This function is called whenever PlotMaker needs to process one
    # data set. It's name is given in parameter here.
    def process_data_set(set)
      # First, we get the dataset:
      begin
        data = xy_data_set(set)
      rescue Exception => ex
        error "Problem when processing set #{set} " +
          "(backend #{backend.class}): #{ex.message}"
        debug "Error backtrace: #{ex.backtrace.join "\n"}"
        warn "Simply ignoring set #{set}"
        return                  # Simply return.
      end
        
      # Scale plots if necessary:
      data.mul!(:x,x_factor) if x_factor
      data.mul!(:y,y_factor) if y_factor

      # Add an offset
      data.add!(:x,x_offset) if x_offset
      data.add!(:y,y_offset) if y_offset

      # Implement a logarithmic scale
      data.safe_log10!(:x) if x_log
      data.safe_log10!(:y) if y_log

      data.strip_nan

      # Then we create a curve object and give it style
      if histogram
        curve = Histogram2D.new(get_current_style(set)) 
      else
        curve = Curve2D.new(get_current_style(set)) 
      end
      if @separate_legends
        @separate_legend_styles << curve.style
      end
      curve.set_data(data)

      # Then we add ot to the current object:
      add_elems_to_current(curve)
      @last_curve = curve       # And we update the last_curve pointer.
    end

    # Provides the name of an output file, given its suffix and
    # optionnaly it base name (defaults to @figure_name).
    def output_filename(suffix, base = @fig_name) 
      if @figmaker.save_dir
        File.join(@figmaker.save_dir,"#{base}#{suffix}")
      else
        "#{base}#{suffix}"
      end
    end

    # Parses _args_ as command-line arguments.
    def parse_command_line_arguments(args)

      # A little trick to allow in-place modification of the
      # command-line from an option.
      @args += args

      # Runs the command to set command-line defaults. Ignored silently
      # in the files given on the command-line.

      quoted_args = args.collect do |s|
        CTioga.shell_quote_string(s)
      end.join ' '

      @command_line = "#{File.basename($0)} #{quoted_args}"

      # Read the CTIOGA environment variable 
      if ENV.key? "CTIOGA"
        ctioga_env = Shellwords.shellwords(ENV["CTIOGA"])
        @parser.parse(ctioga_env)
      end

      if respond_to? :ctioga_defaults
        send :ctioga_defaults
      end

      first = true
      done = false
      while ! done
        begin
          @parser.order!(@args) do |spec|
            # We use Backend#expand to leave room for the backend to
            # interpret the text as many different sets.
            sets = expand_spec(spec)
            info "Expanding #{spec} to #{sets.join(', ')}"
            # Auto --next: 
            next_object if (@auto_next == :element and !first)
            for set in sets
              next_object if (@auto_next == :dataset and !first)
              process_data_set(set)
              first = false
            end
          end
          done = true
        rescue OptionParser::InvalidOption => o
          # Here, we should try to be clever about options:
          new_arg = nil
          option_name = o.args.first.gsub(/^--?/,'')
          # First, we check whether it corresponds to a shortcut:
          if Shortcut.has? option_name
            new_arg = ["--short", option_name]
          # Or an option of the current backend:
          elsif backend.description.param_hash.key? option_name
            new_arg = ["--#{backend.description.name}-#{option_name}"]
          end

          if new_arg
            info "Option #{o.args.first} was reinterpreted " +
              "as #{new_arg.join ' '}"
            @args.unshift(*new_arg)
          else
            debug "Invalid option: #{o.inspect}"
            debug "Remaining args: #{@args.inspect}"
            error "Invalid option: #{o.args.first}. Please run " + 
              "ctioga --help to have a list of valid ones"
            exit 1
          end
        rescue Exception => ex
          # Do not catch normal exit !
          if ex.is_a? SystemExit
            raise
          end
          fatal "ctioga encountered the following problem: #{ex.to_s}"
          debug "Error backtrace: #{ex.backtrace.join "\n"}"
          fatal "Aborting"
          exit 1
        end
      end
    end

    # Setups up various figuremaker variables
    def setup_figure_maker(t)
      # Setting the TeX fontsize if default is not adequate
      t.tex_fontsize = @tex_fontsize if @tex_fontsize

      # add hyperref preamble to have nice PDF comments:
      if @mark
        # We should use the \pdfinfo pdftex primitive, as
        # this will allow to have less surprises...
        t.tex_preview_preamble += 
          "\n\\pdfinfo {\n  /Title (" +
          CTioga.pdftex_quote_string(@command_line) +
          ")\n" + "/Creator(" +
          CTioga.pdftex_quote_string("ctioga #{Version::version}") +
          ")\n}\n"
      end

      if @debug_patterns
        debug_patterns(t)
      end

      # We use Vincent's algorithm for major ticks when available ;-)...
      begin
        t.vincent_or_bill = true
        info "Using Vincent's algorithm for major ticks"
      rescue
        info "Using Bill's algorithm for major ticks"
      end


      return unless @root_object.number > 0 

      # Asking the theme to do something.
      theme_bod_hook(t)

      # We put the command line used in the .tex file:
      t.tex_preview_preamble += 
        "% Produced by ctioga, with the command-line\n" +
        "% #{@command_line.gsub(/\n/,"\n% ")}\n"
      
      if decimal_separator
        t.tex_preamble += <<'EOD'.gsub(',',decimal_separator) # Neat !
\def\frenchsep#1{\frenchsepma#1\endoffrenchsep} 
\def\fseat#1{\frenchsepma}
\def\frenchsepma{\futurelet\next\frenchsepmw}
\def\frenchsepmw{\ifx\next\endoffrenchsep\let\endoffrenchsep=\relax%
\else\if\next.\ifmmode\mathord,\else,\fi%
\else\next\fi\expandafter
\fseat\fi}
EOD
        t.yaxis_numeric_label_tex = t.
          yaxis_numeric_label_tex.gsub('#1','\frenchsep{#1}')
        t.xaxis_numeric_label_tex = t.
          xaxis_numeric_label_tex.gsub('#1','\frenchsep{#1}')
      end

      # Add custom preamble:
      t.tex_preview_preamble += @preamble

      # If the initialization function was found, we call it
      if respond_to? :ctioga_init
        send :ctioga_init, t
      end

      for funcall in @init_funcalls
        funcall.do(t)
      end

    end

    # Runs the code to make the actual figure.
    def do_figure(t)
      @root_object.do(t)
    end

    # Ctioga's banner
    def banner
      <<"EOBANNER" 
This is ctioga version #{Version::version}
ctioga is copyright (C) 2006, 2007, 2008 by Vincent Fourmond 

ctioga comes with absolutely NO WARRANTY. This is
free software, you are welcome to redistribute it under certain
conditions. See the COPYING file in the original tarball for
details. You are also welcome to contribute if you like it, see in
the manual page where to ask.   
EOBANNER
    end

    # Output a figure with the given name
    def output_figure(fig_name)
      # now, we plot the stuffs... if there is actually
      # something to be plotted ;-) !

      @figmaker = t = FigureMaker.new
      # Cleanup code off by default, as this could cause problems for
      # legend pictures generation/EPS generation.
      # The files will get cleaned up anyway.
      t.autocleanup = false


      # Now, we must use real_size stuff !! (default 12cmx12cm)
      # This is not part of setup_figure_maker, as it can strongly interfere
      # when ctioga is used to produce a figure.
      info "Producing PDF file of real size #{@real_size}"
      setup_real_size(t) 

      # And, after that, the generic setup of the FigureMaker object.
      setup_figure_maker(t)
      

      t.def_figure(fig_name) {
        do_figure(t)
      }

      debug "Contents of the main FigureMaker object: " +
        figmaker_options(t)

      info "Generating file '#{fig_name}.pdf'"
      if @fast
        # It isn't that fast, actually.
        info "Producing a fast version"
        # We output only the temporary PDF file
        class << t
          undef show_text
          # We replace show_text by show_marker, to at least get text
          # Doesn't work for axes and ticks... Pity ! 
          def show_text(dict)
            for key in %w{side shift position pos justification}
              dict.delete key
            end
            show_marker(dict)
          end
        end
        t.create_figure_temp_files(t.figure_index(fig_name))
        base_name = if t.save_dir
                      File.join(t.save_dir,fig_name)
                    else
                      fig_name
                    end
        # Remove the _figure.txt files
        File.rename(base_name + "_figure.pdf", base_name + ".pdf")
      else
        t.make_preview_pdf(t.figure_index(fig_name))
      end

      if @eps
        info "Attempting to create an EPS file, watch out error messages"
        # First, we convert the _figure.pdf into eps with pdftops
        cmd = "pdftops -eps #{output_filename('_figure.pdf', fig_name)}"
        debug "Running #{cmd}"
        system cmd
        f = output_filename('.tex', fig_name)
        new = output_filename('.new.tex', fig_name)
        debug "Modifying #{f}"
        orig = open(f)
        out = open(new, "w")
        for l in orig
          l.gsub!('pdftex,','') # \usepackage[pdftex,...]...
          l.gsub!('_figure.pdf', '_figure.eps')
          out.print l
        end
        orig.close
        out.close

        spawn "latex #{new}"
        spawn "dvips -o #{output_filename('.eps', fig_name)} -t unknown -T " +
          "#{@real_size.gsub(/x/,',')} #{output_filename('.new.dvi', fig_name)}"
      end

      if @png
        # We create the PNG file:
        spawn "convert -density #{(@png_oversampling * 72).to_i} "+
          "#{output_filename('.pdf', fig_name)} -resize " +
          "#{@png_size.join('x')} #{output_filename('.png', fig_name)}"
      end

      if @svg
        # We create the SVG file
        spawn "pdf2svg #{output_filename('.pdf', fig_name)} " +
          "#{output_filename('.svg', fig_name)}"
      end

      
      # Generating the separate legends if requested
      if not @separate_legend_styles.empty?
        index = 0
        info "Creating separate lengends"
        for style in @separate_legend_styles 
          name = sprintf "%s_legend_%02d", fig_name, index
          index += 1
          t.def_figure(name) {
            t.set_device_pagesize(@separate_legend_width * 10,
                                  @separate_legend_height * 10)
            t.set_frame_sides(0,1,1,0) 
            t.update_bbox(0,0)
            t.update_bbox(1,1)
            style.output_legend_pictogram(t)
          }
          # create_figure takes a number. It is part of Tioga's internals.
          t.create_figure_temp_files(t.figure_index(name))
          

          # Some post_processing to rename the files appropriately
          # and delete the unwanted _figure.txt file
          base_name = if t.save_dir
                        File.join(t.save_dir,name)
                      else
                        name
                      end
          # Remove the _figure.txt files
          begin
            File.rename(base_name + "_figure.pdf", base_name + ".pdf")
            if @eps             # Create also EPS files:
              spawn "pdftops -eps #{base_name}.pdf"
            end
            File.delete(base_name + ".tex")
            File.delete(base_name + "_figure.txt")
          rescue
            warn "Files should have been deleted, but were not found"
          end
        end
      end

      if @cleanup
        files_to_remove = %w(.tex .out .aux .log _figure.pdf _figure.txt) +
          %w(.new.tex .new.aux .new.dvi .new.log _figure.eps) # EPS-related files
      elsif @tex_cleanup
        files_to_remove = %w(.tex .out .aux .log .pdf)
      elsif @clean_all
        files_to_remove = %w(.tex .out .aux .log .pdf _figure.pdf _figure.txt) +
          %w(.new.tex .new.aux .new.dvi .new.log .eps _figure.eps) # EPS-related files

      end
      if @viewer 
        # we display the output file:
        info "Starting the viewer #{@viewer} as requested"
        pdf_file = if t.save_dir
                     File.join(t.save_dir,fig_name+".pdf")
                   else
                     fig_name+".pdf"
                   end
        system("#{@viewer} #{pdf_file}")
      end
      
      if files_to_remove
        files_to_remove.collect! {|s| 
          output_filename(s, fig_name)
        }
        files_to_remove << "pdflatex.log" 
        info "Cleaning up temporary files"
        debug "Cleaning #{files_to_remove.join ' '}"
        files_to_remove.each do |f| 
          begin
            File.delete(f)
          rescue
            debug "File #{f} absent, not deleted" 
          end
        end
      end

    end

    # Runs the plots, using the arguments given in the command-line.
    # Now delegates to some other small functions.
    def run(args)

      # First, we parse command-line arguments:
      parse_command_line_arguments(args)

      # Then, we spit the output:
      output_figure(@fig_name)

    end

  end

  # Takes a string a returns a quoted version that should be able
  # to go through shell expansion. For now, it won't work with
  # strings containing ', but that's already a good help.
  def CTioga.shell_quote_string(str)
    if str =~ /[\s"*$()\[\]{}';]/
      if str =~ /'/
        a = str.gsub(/(["$\\])/) { "\\#$1" }
        return "\"#{a}\""
      else 
        return "'#{str}'"
      end
    else
      return str
    end
  end

  # This function provides a 'command-line' interface from within Tioga !
  def CTioga.define_tioga_figure(t, name, *args)
    t.def_figure(name) {
      CTioga.show_tioga_plot(t, *args)
    }
  end

  # This function provides a 'command-line' interface from within Tioga !
  def CTioga.show_tioga_plot(t, *args)
    plot = PlotMaker.new
    plot.parse_command_line_arguments(args)
    plot.setup_figure_maker(t)
    
    width = 72 * t.
      convert_output_to_inches(t.
                               convert_figure_to_output_dx(t.convert_frame_to_figure_dx(1)))
    height = 72 * t.
      convert_output_to_inches(t.
                               convert_figure_to_output_dy(t.convert_frame_to_figure_dy(1)))

    plot.set_root_frame(0, width, height, 0)
    
    plot.do_figure(t)
  end

  
end

