(* Copyright (C) 2001-2009

     Christophe Troestler
     email: Christophe.Troestler@umh.ac.be
     WWW: http://math.umh.ac.be/an/software/

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public License
   version 2.1 as published by the Free Software Foundation, with the
   special exception on linking described in file LICENSE.

   This library 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 file
   LICENSE for more details.
*)


(**********************************************************************
 *
 * Data structures & basic functions
 *
 **********************************************************************)

open Printf

type color = int

exception Exit of Unix.process_status

let is_finite x =
  match classify_float x with
  | FP_infinite | FP_nan -> false
  | _ -> true

(* [force_show gp] force the drawing of axes, tics,... by plotting an
   "empty" file.  That will only work IF the RANGES have been SET.
   Beware that, unless multiplot mode is on, this will also erase all
   the plots.  (Note that the missing symbol must be declared as "?".)
*)
let force_show gp =
  output_string gp "plot '-' title \"\"\n? ?\ne;\n"

(*
 * Plot history
 **********************************************************************)

(* Plots will be grouped by tags.  When no tag is provided, the system
   one will be assigned -- which implies that by default the plots
   will be managed automatically by the system. *)
type tag = int

module Tags = Map.Make(struct
                         type t = tag
                         let compare (x:tag) y = compare x y
                       end)

type tag_state =
  | Managed  (* Visible and the system determines if replot is needed *)
  | Visible  (* Set visible by user -- only reploted if the user ask for it *)
  | Hidden   (* Erased, not on display anymore *)
  | To_show  (* scheduled to make Visible -- by [show] or a plot command *)
  | To_hide  (* scheduled to erase *)

(* Experiments show that passing the data through the pipe to gnuplot
   is not efficient unless there are very few lines of data. *)
type data =
  | Inlined of string (* data in the form "line1\nline2\n...\ne\n" *)
  | File of string    (* filename containing the data *)

type plot = string * float * int * color * data
    (* [(cmd, lw, lt, color, data)] where [cmd] is something like
       ["\"datafile\" axes x1y2 title \"\" with pt 1 ps 2"], [lw] is
       the line width, and [lt] the value of the linetype of the plot.
       The gnuplot command to show such a plot is therefore [sprintf
       "plot %s lw %F lt %i" cmd lw lt].  If the [data] is inlined,
       one must follow the command "plot..." with the data in the same
       order than the '-'. *)

type text = string * int
    (* [(cmd, gp_tag)] where [cmd] is something like ["set label
       \"...\" ... tc lt 2"] and [gp_tag] is the gnuplot tag (NOT the
       tag of this library) and we will subsequently refer to it as
       the label number.  *)

type plots = {
  mutable state : tag_state;
  plots : plot Queue.t;
  texts : text Queue.t;
}

(* [string_of_queue init q] will add to [init] (assumed to end with
   ",") the instructions to plot all data in the queue [q].  *)
let string_of_queue init q =
  Queue.fold (fun cmd (s, lw, lt, color, _) ->
                sprintf "%s%s lw %F lt %i %s," cmd s lw lt
                  (if color >= 0 then sprintf "lc rgb \"#%X\"" color else "")
             ) init q.plots

(* NOTE: The border linewidth (-1) is by default twice as fat as the
   other ones.  Scale appropriately when erasing. *)
let string_of_queue_hide init q =
  Queue.fold (fun cmd (s, lw, lt, color, _) ->
                let lw = if color >= 0 && lt = -1 then 2. *. lw else lw in
                sprintf "%s%s lw %F lt -2," cmd s lw) init q.plots

let ouput_texts gp q =
  Queue.iter (fun (cmd, _) ->
                output_string gp cmd; output_char gp '\n') q.texts

(* [show_plot_queue s_of_q plot gp q] send to [gp] the command to plot
   all the graphs in the queue [q].  [plot] is either "plot " or
   "splot " (note the trailing space).

   REMARK: the linetype "-2" has the color of the background. *)
let show_or_hide_plot_queue s_of_q plot gp q=
  if Queue.is_empty q.plots then force_show gp else begin
    let cmd = s_of_q plot q in
    cmd.[String.length cmd - 1] <- '\n';
    output_string gp cmd;
    (* Inline data *)
    Queue.iter (function
                | (_, _, _, _, Inlined d) -> output_string gp d
                | _ -> ()) q.plots
  end

let show_plot_queue = show_or_hide_plot_queue string_of_queue
let hide_plot_queue = show_or_hide_plot_queue string_of_queue_hide

(* [empty_plot_queue q] frees all data and temp files for queue [q]. *)
let empty_plot_queue q =
  Queue.iter (function
              | (_, _, _, _, File f) -> (try Unix.unlink f with _ -> ())
              | _ -> ()) q.plots;
  Queue.clear q.plots


type plot_history = plots Tags.t

(* [show_plots_all_tags plots gp sp] will send the command to the
   channel [gp] to redraw all the (visible) plots of the subpages [sp]
   starting with no plots.  As we may be in autoscale mode, one must
   group all plots into a _single_ plot instruction to ensure Gnuplot
   sets the ranges correctly (otherwise, several ranges may
   superimpose).  *)
let show_plots_all_tags =
  let add_queue init q =
    match q.state with
    | Managed | Visible -> string_of_queue init q
    | To_show -> (q.state <- Visible; string_of_queue init q)
    | Hidden -> init
    | To_hide -> (q.state <- Hidden; init) in
  fun plot gp history ->
    let cmd = Tags.fold (fun _ q cmd -> add_queue cmd q) history "" in
    if cmd = "" then
      force_show gp (* FIXME: does not work if ranges are not set
                       (see update_show) *)
    else begin
      cmd.[String.length cmd - 1] <- '\n';
      output_string gp plot;
      output_string gp cmd;
      (* Inline data *)
      Tags.iter (fun _ q ->
                   Queue.iter (function
                               | (_, _, _, _, Inlined d) -> output_string gp d
                               | _ -> ()) q.plots
                ) history
    end


(* Maximum number of lines of data for it to be inlined; if the data
   has more than [max_inline] lines, a temporary file is created.  The
   following value has been determined experimentally.  *)
let max_inline = 6

(* The tag for system managed plots.  It will stay in the state
   [Managed] and not be accessible to the user. *)
let system_tag = 0


(* Axes
 **********************************************************************)

(* Axis of a box.  Each axis is independent of the others and consists
   of the following characteristics:
*)
type axis = {
  mutable draw : bool;    (* whether to draw the border *)
  mutable draw_top : bool;(* whether to draw the top border (3D) *)
  mutable zero : bool;    (* whether to draw the zero axis *)
  mutable min : float;    (* [min:max] interval for that axis *)
  mutable max : float;    (* +- infty means autoscale *)
  mutable tics : bool;    (* whether to draw major tics *)
  (* FIXME: tics on zeroaxis??? *)
  mutable tics_step : float; (* <= 0 means automatic *)
  mutable mtics : int;    (* how many minor tics *)
  mutable format : string;(* tics labels format; "" = no label *)
  mutable format_rot : bool; (* true if rotate the label *)
  mutable format_font : string; (* font of tics labels, ex: ["Arial,12"] *)
  mutable format_lt : int; (* gnuplot linetype; color of tics labels *)
  mutable format_color : color; (* color (if >=0, supersedes _lt) *)
  mutable grid : bool;    (* whether a grid is drawn at each major tic *)
  mutable mgrid : bool;   (* whether a grid is drawn at each minor tic *)
  mutable logscale : bool;(* whether to put label logarithmically *)
  mutable active : bool;  (* whether the axis is "active" -- labelled
                             by the *last* [box] command *)
  mutable label : string; (* label for that axis with style (except color),
                             e.g. ["\"x\" font \"Helvetica,default\""]
                             ("\"\"" = none) *)
  mutable label_lt : int; (* the integer is the linetype (gives
                             the color of the text). *)
  mutable label_color : color; (* color of the label; supersede [label_lt]. *)
  mutable label_nlines : int; (* number of lines of the label *)
  mutable label_shown : bool; (* Whether the label has been displayed
                                 (i.e. set + plot) *)
  mutable label_old : string;
  (* The currently _displayed_ label to erase (and to replace with
     [label]).  "" means nothing to erase. *)
}


(* Subpages parameters & basic funtions
 **********************************************************************)

(* Each subpage determines an area where the plotting takes place.

   Unfortunately Gnuplot makes it impossible to correctly set a
   viewport ("box" where the graphs are plotted).  First, margins
   units are chars width and heights (that could be worked around) but
   more importantly they do not obey to fractional sizes (e.g. set
   bmargin 1.4).  As a consequence, one has to let Gnuplot choose the
   viewport.  That implies one must be careful when to redraw the
   entire graph: indeed, each plot command will redraw the
   box,... according to the current parameters -- we must therefore
   keep the entire history.
*)
type subpage = {
  set_subpage : string; (* gnuplot cmd to set the position of the subpage *)
  subpage_xmin : float; (* The coordinates of the subpage; *)
  subpage_ymin : float; (* the dims are hold in the handle since they
                           are the same for all subpages. *)
  mutable redraw : bool;
  (* This flag indicates whether the entire plot must be redrawn. *)

  mutable xmin : float; (* Since the range is set with [win] before [box] *)
  mutable xmax : float; (* can set the active axes, we need a way to store *)
  mutable ymin : float; (* the current coordinates.  +-infty = autoscale *)
  mutable ymax : float;
  x1 : axis; (* bottom *)
  x2 : axis; (* top *)
  y1 : axis; (* left *)
  y2 : axis; (* right *)
  mutable tics_outwards : bool; (* whether tics are drawn outward.
                                   This is global under gnuplot. *)

  mutable title : string; (* title -- see [label] above. *)
  mutable title_nlines : int;
  mutable title_lt : int;
  mutable title_color: color; (* RGB color, if >=0, supersede title_lt *)
  mutable title_shown : bool;
  mutable title_old : string;
  mutable text_number : int; (* the next text number to use for gnuplot
                                in "set label ..." commands. *)
  mutable tags : plot_history; (* history of plotting commands *)

  mutable plot3D : bool; (* says whether the current plot is 3D *)
  mutable zmin : float;
  mutable zmax : float;
  z1 : axis; (* vertical direction for 3D *)
  z2 : axis; (* "front" vertical direction (not everything is used) *)
}


(* [add_plot_with_tag sp t plot] add the plot [plot] to the tag [t] in
   the subpage [sp].  The queue is returned for possible further
   processing.  The tag is created with [Hidden] state if necessary.
   The reasoning for the default state [Hidden] is that tagging plots
   is often used to group them, hence the display should take place
   after all the plots have been tagged.  *)
let add_plot_with_tag sp t plot =
  try
    let q = Tags.find t sp.tags in
    Queue.add plot q.plots;
    q
  with Not_found ->
    let pl = Queue.create() in
    Queue.add plot pl;
    let q = { state = Hidden; plots = pl; texts = Queue.create() } in
    sp.tags <- Tags.add t q sp.tags;
    q


(* [add_text_with_tag sp t text] does the same as [add_plot_with_tag]
   but for text instead of plots. *)
let add_text_with_tag sp t text =
  try
    let q = Tags.find t sp.tags in
    Queue.add text q.texts;
    q
  with Not_found ->
    let txt = Queue.create() in
    Queue.add text txt;
    let q = { state = Hidden; plots = Queue.create(); texts = txt } in
    sp.tags <- Tags.add t q sp.tags;
    q


(* [update_labels gp sp] sends the Gnuplot commands through [gp] to
   carefully erase (immediately) the old the labels and title and
   set the new ones.  This will in 2D and 3D but only work if the
   ranges are set and border, tics,... are unchanged. *)
let update_labels =
  let erz_label gp modif xy a =
    if a.label_old <> "" then begin
      fprintf gp "set %slabel %s tc lt -2;" xy a.label_old;
      modif := true
    end in
  let new_label gp xy a =
    if a.label_old <> "" then begin
      fprintf gp "set %slabel %s tc %s;" xy a.label
        (if a.label_color >= 0 then sprintf "rgb \"#%X\"" a.label_color
         else sprintf "lt %i" a.label_lt);
      a.label_old <- ""
    end in
  fun gp sp ->
    (* Erase old labels *)
    let modif = ref false in
    erz_label gp modif "x" sp.x1;
    erz_label gp modif "x2" sp.x2;
    erz_label gp modif "y" sp.y1;
    erz_label gp modif "y2" sp.y2;
    erz_label gp modif "z" sp.z1;
    if sp.title_old <> "" then begin
      fprintf gp "set title %s tc lt -2;\n" sp.title_old;
      modif := true
    end;
    if !modif then begin
      force_show gp; (* force erasing only if modif to do *)
      (* Set new labels -- for those erased only *)
      new_label gp "x" sp.x1;
      new_label gp "x2" sp.x2;
      new_label gp "y" sp.y1;
      new_label gp "y2" sp.y2;
      new_label gp "z" sp.z1;
      if sp.title_old <> "" then begin
        fprintf gp "set title %s tc %s;\n" sp.title
          (if sp.title_color >= 0 then sprintf "rgb \"#%X\"" sp.title_color
           else sprintf "lt %i" sp.title_lt);
        sp.title_old <- ""
      end
    end

let range_of_float f =
  if is_finite f then string_of_float f else "*"

(* [set_box2D gp sp] sends the Gnuplot commands to [gp] to set the
   box, axes, and title according to the information in the subpage
   [sp].  Only the labels that are UNCHANGED are set, the other ones
   will be set after the old ones are deleted.  This information needs
   to be resent to Gnuplot when one changes subpages for example. *)
let set_box2D gp sp =
  output_string gp (if sp.tics_outwards then "set tics out;"
                    else "set tics in;");
  let borders =
    (if sp.x1.draw then 1 else 0) + (if sp.x2.draw then 4 else 0)
    + (if sp.y1.draw then 2 else 0) + (if sp.y2.draw then 8 else 0) in
  output_string gp ("set border " ^ (string_of_int borders) ^ ";\n");
  let axis xy a =
    let setxy = "set " ^ xy
    and unsetxy = "unset " ^ xy in
    output_string gp ((if a.zero then setxy else unsetxy) ^ "zeroaxis;");
    fprintf gp "set %srange [%s:%s];"
      xy (range_of_float a.min) (range_of_float a.max);
    if a.tics then
      fprintf gp "set %stics border nomirror %srotate %s font \"%s\" tc %s;"
        xy
        (if a.format_rot then "" else "no")
        (if a.tics_step <= 0. then "" else string_of_float a.tics_step)
        a.format_font
        (if a.format_color >= 0 then sprintf "rgb \"#%X\"" a.format_color
         else sprintf "lt %i" a.format_lt)
    else
      output_string gp (unsetxy ^ "tics;");
    if a.mtics > 0 then fprintf gp "set m%stics %i;" xy a.mtics
    else fprintf gp "unset m%stics;\n" xy;
    fprintf gp "set format %s %S;\n" xy a.format;
    fprintf gp "set grid %s%stics %sm%stics front;"
      (if a.grid then "" else "no") xy (if a.mgrid then "" else "no") xy;
    output_string gp ((if a.logscale then "set logscale "
                       else "unset logscale ") ^ xy ^ ";");
    if a.label_old = "" then begin
      fprintf gp "set %slabel %s tc %s;\n" xy a.label
        (if a.label_color >= 0 then sprintf "rgb \"#%X\"" a.label_color
         else sprintf "lt %i" a.label_lt);
      a.label_shown <- true
    end in
  axis "x"  sp.x1;
  axis "x2" sp.x2;
  axis "y"  sp.y1;
  axis "y2" sp.y2;
  if sp.title_old = "" then
    fprintf gp "set title %s tc %s;\n" sp.title
      (if sp.title_color >= 0 then sprintf "rgb \"#%X\"" sp.title_color
       else sprintf "lt %i" sp.title_lt)


(* [set_box3D gp sp] is the same as [set_box2D] but for 3D plots.  *)
let set_box3D gp sp =
  output_string gp (if sp.tics_outwards then "set tics out;"
                    else "set tics in;");
  (* BEWARE: the borders are not linked to axes but to the view; set a
     view so that they initially correspond. *)
  output_string gp "set view 56,43;";
  let borders =
    (if sp.x1.draw then 1 else 0) + (if sp.x2.draw then 8 else 0)
    + (if sp.y1.draw then 2 else 0) + (if sp.y2.draw then 4 else 0)
    + (if sp.x1.draw_top then 1024 else 0)
    + (if sp.x2.draw_top then 512 else 0)
    + (if sp.y1.draw_top then 256 else 0)
    + (if sp.y2.draw_top then 2048 else 0)
    + (if sp.z1.draw then 16 else 0) + (if sp.z1.draw_top then 128 else 0)
    + (if sp.z2.draw then 32 else 0) + (if sp.z2.draw_top then 64 else 0) in
  output_string gp ("set border " ^ (string_of_int borders) ^ ";\n");
  let axis xy a a2 =
    (* let setxy = "set " ^ xy in *)
    let unsetxy = "unset" ^ xy in
    fprintf gp "set %srange [%s:%s];"
      xy (range_of_float a.min) (range_of_float a.max);
    if a.tics then
      fprintf gp "set %stics border %smirror %s font \"%s\" tc %s;"
        xy
        (if a2.tics then "" else "no")
        (if a.tics_step <= 0. then "" else string_of_float a.tics_step)
        a.format_font
        (if a.format_color >= 0 then sprintf "rgb \"#%X\"" a.format_color
         else sprintf "lt %i" a.format_lt)
    else
      output_string gp (unsetxy ^ "tics;");
    if a.mtics > 0 then fprintf gp "set m%stics %i;" xy a.mtics
    else fprintf gp "unset m%stics;" xy;
    fprintf gp "set format %s %S;" xy a.format;
    fprintf gp "set grid %s%stics %sm%stics front;"
      (if a.grid then "" else "no") xy (if a.mgrid then "" else "no") xy;
    output_string gp ((if a.logscale then "set logscale "
                       else "unset logscale ") ^ xy ^ ";");
    if a.label_old = "" then begin
      fprintf gp "set %slabel %s tc %s;\n" xy a.label
        (if a.label_color >= 0 then sprintf "rgb \"#%X\"" a.label_color
         else sprintf "lt %i" a.label_lt);
      a.label_shown <- true
    end in
  axis "x" sp.x1 sp.x2;
  axis "y" sp.y1 sp.y2;
  axis "z" sp.z1 sp.z2;
  if sp.title_old = "" then
    fprintf gp "set title %s tc %s;\n" sp.title
      (if sp.title_color >= 0 then sprintf "rgb \"#%X\"" sp.title_color
       else sprintf "lt %i" sp.title_lt)


(* [is_autoscaled2D sp] says whether an axis is autoscaled.  This is
   important to know whether the entire plot must be redrawn. *)
let is_autoscaled2D =
  let axis a = is_finite a.min && is_finite a.max in
  fun sp ->
    not(axis sp.x1 && axis sp.x2 && axis sp.y1 && axis sp.y2)

let is_autoscaled3D =
  let axis a = is_finite a.min && is_finite a.max in
  fun sp ->
    not(axis sp.x1 && axis sp.x2 && axis sp.y1 && axis sp.y2 && axis sp.z1)

(* [moveto_subpage gp sp] sends the Gnuplot commands through [gp] to
   define the subpage [sp] "area" and reset its parameters -- but does
   NOT clear it; this is the task of the plotting command. *)
let moveto_subpage gp sp =
  output_string gp sp.set_subpage;
  if sp.plot3D then set_box3D gp sp else set_box2D gp sp;
  (* Set subpage labels that must be visible -- the [To_show] will be
     updated later. *)
  output_string gp "unset label;\n";
  Tags.iter (fun  _ q -> match q.state with
             | Managed | Visible -> ouput_texts gp q
             | _  -> ()) sp.tags


(* Gnuplot handle
 **********************************************************************)

type handle = {
  to_gplot : out_channel; (* channel to send commands to gnuplot *)
  gp_extra : Gnuplot_sys.t;
  mutable closed : bool; (* says whether the handle is closed.  FIXME: Every
                            function should check this first. *)
  interactive : bool; (* if true, the commands are issued immediately
                         to gnuplot. *)
  max_inline : int; (* maximum of lines that are inlined (sent through
                       the pipe); if there are more lines, a temp file
                       is created. *)
  buf_inline : Buffer.t; (* buffer to hold the plot loop -- 1 per session *)
  newpage : string; (* gnuplot cmd to be issued to start a new page
                       FIXME: reevaluate whether it is needed. *)

  nxsub : int;  nysub : int;   (* number of subpages in x and y directions *)
  subw : float; subh : float;  (* width and height of subpages *)
  mutable sub : int; (* current subpage number: 0,..., nxsub*nysub - 1 *)
  subpage : subpage array;    (* of length nxsub*nysub *)

  mutable pen : int;           (* Current pen *)
  mutable color : color; (* Current active color (if >=0,
                            otherwise use pen) *)
  mutable pen_width : float;   (* Current pen width *)
  mutable point : int;         (* Current point type *)
  mutable point_width : float; (* Current point width *)
  mutable font : string;       (* Current font ("" = default) *)
  mutable font_size : int;     (* Current font size *)
}


let string_of_font g =
  g.font ^ (if g.font_size <= 0 then ""
            else "," ^ string_of_int g.font_size)



(*
 * Output devices
 **********************************************************************)

type device =
  | X
  | Wxt
  | PS of string   (* Postscript *)
  | EPS of string  (* Encapsulated PostScript *)
  | EPSLaTeX of string (* picture environment including a PS file *)
  | FIG of string  (* Xfig format *)
  | PNG of string  (* Portable Network Graphics *)
  | MP of string   (* Metapost *)
  | MF of string   (* Metafont *)
  | SVG of string  (* Scalable Vector Graphics *)

let device_of_filename file =
  let suffix_is = Filename.check_suffix file in
  if suffix_is ".ps"  then PS(file)
  else if suffix_is ".eps" then EPS(file)
  else if suffix_is ".tex" then EPSLaTeX(file)
  else if suffix_is ".fig" then FIG(file)
  else if suffix_is ".png" then PNG(file)
  else if suffix_is ".mp"  then MP(file)
  else if suffix_is ".mf"  then MF(file)
  else if suffix_is ".svg" then SVG(file)
  else failwith "Gnuplot.device_of_filename"



(*
 * Plots management
 **********************************************************************)

(* See [update_show] for a description.  This function implements the
   main logic for updating plots, labels,...
*)
let update_show_generic sp ~plot_cmd ~redraw ~set_box ?add_plot g =
  (* If we are not in interactive mode, the plots are made at newpages
     so there is never anything to update. *)
  if g.interactive then begin
    if redraw sp then begin
      sp.redraw <- false;
      (* Clear *)
      if g.nxsub = 1 && g.nysub = 1 then output_string g.to_gplot g.newpage
      else output_string g.to_gplot "clear;\n";
      (* All labels and plots have just been erased; mark them as
         such.  Redraw box and all [Managed], [Visible], [To_show]
         plots.  We need to redraw them as a single plot command to
         avoid superimposing (multiplot) multiple ranges (autoscale)
         on the same plot.  *)
      sp.x1.label_old <- "";
      sp.x2.label_old <- "";
      sp.y1.label_old <- "";
      sp.y2.label_old <- "";
      sp.z1.label_old <- "";
      sp.title_old <- "";
      begin match add_plot with
      | Some(t,plot) -> ignore(add_plot_with_tag sp t plot);
      | None -> ()
      end;
      set_box g.to_gplot sp;
      show_plots_all_tags plot_cmd g.to_gplot sp.tags;
      flush g.to_gplot
    end
    else begin
      (* Erase carefully labels and plots *)
      update_labels g.to_gplot sp;
      let del_sth = Tags.fold (fun _ q del -> match q.state with
                               | To_hide ->
                                   hide_plot_queue plot_cmd g.to_gplot q;
                                   q.state <- Hidden;
                                   true
                               | _ -> del) sp.tags false in
      (* Redraw managed plots (in case erasing destroyed them). *)
      if del_sth then
        Tags.iter (fun _ q -> match q.state with
                   | Managed -> show_plot_queue plot_cmd g.to_gplot q
                   | _ -> ()) sp.tags;
      (* Add the new plot (if any) and possibly show it. *)
      begin match add_plot with
      | Some(t, ((s, lw, lt, color, data) as plot)) ->
          let q = add_plot_with_tag sp t plot in
          begin match q.state with
          | Managed | Visible ->
              fprintf g.to_gplot "%s %s lw %F lt %i%s;\n" plot_cmd s lw lt
                (if color >= 0 then sprintf "lc rgb \"#%X\"" color
                 else "");
              (match data with
               | Inlined d -> output_string g.to_gplot d
               | _ -> ())
          | _ -> ()
          end
      | None -> ()
      end;
      (* We also need to process any waiting plots.  However, we do not
         replot the [Visible] plots, they are under user's control. *)
      Tags.iter (fun _ q -> match q.state with
                 | To_show ->
                     show_plot_queue plot_cmd g.to_gplot q;
                     q.state <- Visible
                 | _ -> ()) sp.tags;
      flush g.to_gplot
    end
  end
  else begin
    (* Not interactive mode, just add the plot *)
    match add_plot with
    | Some(t,plot) -> ignore(add_plot_with_tag sp t plot);
    | None -> ()
  end



(* [update_show ?add_plot g] will update the display of the plots.
   That is any plot waiting to be erased or to be shown will be.
   Managed plots may be re-plotted if needed.

   @param add_plot must be a couple (tag, plot).  If present, the plot
   will be added to the tag (the latter will be created if necessary)
   and displayed according to the state of the tag.
*)
let update_show ?add_plot g =
  let sp = g.subpage.(g.sub) in
  if sp.plot3D then
    update_show_generic sp ?add_plot g
      ~plot_cmd:"splot"
      ~redraw:(fun sp -> sp.redraw || is_autoscaled3D sp)
      ~set_box:set_box3D
  else
    update_show_generic sp ?add_plot g
      ~plot_cmd:"plot"
      ~redraw:(fun sp -> sp.redraw || is_autoscaled2D sp)
      ~set_box:set_box2D



(*
 * Gnuplot initializing, closing and (sub)pages
 **********************************************************************)


let inches_per_mm = 1. /. 25.4

let string_of_current_time () =
  let t = Unix.localtime(Unix.time()) in
  sprintf "%4i-%02i-%02i %i:%02i:%02i"
    (1900 + t.Unix.tm_year) (1 + t.Unix.tm_mon) t.Unix.tm_mday
    t.Unix.tm_hour t.Unix.tm_min t.Unix.tm_sec


let init ?offline ?(max_inline=max_inline) ?(persist=true) ?(color=true)
  ?(nxsub=1) ?(nysub=1) ?xsize ?ysize ?(aspect=1.) dev =
  (* Get the sizes *)
  let (xsize, ysize) = match xsize, ysize with
    | Some(x), Some(y) ->
        if x <= 0. then invalid_arg "Gnuplot.init: xsize <= 0"
        else if y <= 0. then invalid_arg "Gnuplot.init: ysize <= 0"
        else (x,y)
    | Some(x), None ->
        if x <= 0. then invalid_arg "Gnuplot.init: xsize <= 0"
        else if aspect <= 0. then invalid_arg "Gnuplot.init: aspect <= 0"
        else (x, aspect *. x)
    | None, Some(y) ->
        if aspect <= 0. then invalid_arg "Gnuplot.init: aspect <= 0"
        else if y <= 0. then invalid_arg "Gnuplot.init: ysize <= 0"
        else (y /. aspect, y)
    | None, None ->
        let x = match dev with
          | X | Wxt | PNG _ | SVG _ -> 550. (* pixels *)
          | PS _ | EPS _ | EPSLaTeX _ | MP _ | MF _ | FIG _ ->
              100. (* milimeters *) in
        if aspect <= 0. then invalid_arg "Gnuplot.init: aspect <= 0"
        else (x, aspect *. x) in
  (* Open a pipe to gnuplot *)
  let (gp, extra), offline = match offline with
    | None -> Gnuplot_sys.open_gnuplot_out persist xsize ysize color, false
    | Some(plt) -> Gnuplot_sys.open_file_out plt, true in
  (* Global settings *)
  if offline then
    fprintf gp "# Generated by OCaml Gnuplot module on %s.\n"
      (string_of_current_time());
  output_string gp "set locale \"C\";\n";
  output_string gp "set encoding iso_8859_1;\n";
  output_string gp "set decimalsign '.';\n";
  output_string gp "set datafile missing \"?\";\n";
  output_string gp "set zero 0.0;\n";
  output_string gp "set hidden3d;\n"; (* FIXME: must be more subtle *)
  (* Set device, output and size *)
  let scolor = if color then "color" else "monochrome" in
  let set_output s = fprintf gp "set output %S;\n" s in
  let w, h, default_font = match dev with
    | X ->
        (* Not setting the terminal amounts to select the default one
           which is what we want for offline output to be able to
           (e.g.) play on windows a script made on Unix. *)
        if not offline then Gnuplot_sys.set_terminal_X gp xsize ysize color;
        (* no need to set output *)
        (1., 1., "default")
    | Wxt ->
        let pgmname = Filename.basename Sys.argv.(0) in
        fprintf gp "set term wxt title \"OCaml Gnuplot: %s\" enhanced \
	  %spersist noraise;\n" pgmname (if persist then "" else "no");
        (1., 1., "default")
    | PS(s) ->
        if xsize >= ysize then begin
          fprintf gp "set terminal postscript landscape enhanced %s;\n" scolor;
          set_output s; (* default size 10x7 inches *)
          (xsize /. 10. *. inches_per_mm, ysize /. 7. *. inches_per_mm,
           "Helvetica")
        end else begin
          fprintf gp "set terminal postscript portrait enhanced %s;\n" scolor;
          set_output s; (* default size 7x10 inches *)
          (xsize /. 7. *. inches_per_mm, ysize /. 10. *. inches_per_mm,
           "Helvetica")
        end
    | EPS(s) ->
        fprintf gp "set terminal postscript eps enhanced %s;\n" scolor;
        set_output s;
        (xsize /. 10. *. inches_per_mm, ysize /. 7. *. inches_per_mm,
         "Helvetica")
    | EPSLaTeX (tex) ->
        let eps = (Filename.chop_suffix tex ".tex") ^ ".eps" in
        fprintf gp "set terminal epslatex %s dashed;\n" scolor;
        set_output eps; (* default size 5x3 inches *)
        (xsize /. 5. *. inches_per_mm, ysize /. 3. *. inches_per_mm,
         "default")
    | FIG(s) ->
        fprintf gp "set terminal fig %s size %.15g %.15g metric dashed \
	  textnormal depth 50;\n"
          scolor (xsize /. 10.) (ysize /. 10.); (* fig: size in centimeters *)
        set_output s;
        (1., 1., "default")
    | PNG(s) ->
        fprintf gp
          "set terminal png transparent medium size %.0f,%0.f enhanced;\n"
          xsize ysize;
        set_output s;
        (1., 1., "medium")
    | MP(s) ->
        fprintf gp "set terminal mp %s dashed tex;\n" scolor;
        set_output s; (* default size 5x3 inches *)
        (xsize /. 5. *. inches_per_mm, ysize /. 3. *. inches_per_mm,
         "cmr10")
    | MF(s) ->
        fprintf gp "set terminal mf;\n";
        set_output s; (* default size 5x3 inches *)
        (xsize /. 5. *. inches_per_mm, ysize /. 3. *. inches_per_mm,
         "cmr10")
    | SVG(s) ->
        fprintf gp "set terminal svg size %F %F dynamic enhanced;\n"
          xsize ysize;
        set_output s;
        (1., 1., "Arial") in
  fprintf gp "set size %.15g,%.15g;\n" w h;
  (* Several plots per page ? *)
  let nxsub = max nxsub 1
  and nysub = max nysub 1 in
  let nsub = nxsub * nysub
  and subw = w /. float nxsub
  and subh = h /. float nysub in
  let begin_new_page =
    sprintf "set multiplot;%sset size %.15g,%.15g;\n"
      (match dev with
       | MP _ | MF _ -> (* Get the size of the output right even if
                           all the subpages are not used. *)
           sprintf "set size %.15g,%.15g; clear;" w h
       | _ -> "")
      subw subh in
  let make_axis () =
    (* BEWARE: this a a mutable structure, so one must create a
       different instance of it for each axis. *)
    {
      draw = false; (* must explicitely ask for axes *)
      draw_top = false;
      zero = false;
      min = neg_infinity; max = infinity; (* autoscale *)
      tics = false;   tics_step = 0.;
      mtics = 0;
      format = "";    format_rot = false;
      format_font = default_font; (* no font size <==> default size *)
      format_lt = -1; (* border color *)
      format_color = -1; (* invalid color (as long as "lt" is possible) *)
      grid = false;   mgrid = false;
      logscale = false;
      active = true;
      label = "\"\""; (* empty label <==> none *)
      label_nlines = 0;
      label_lt = -1; (* border color *)
      label_color = -1; (* invalid color (as long as "lt" is possible) *)
      label_shown = false;
      label_old = ""; (* nothing to erase *)
    } in
  let make_subpage i =
    (* The subpages are counted horizontally, the top left one
       having number [i=0]. *)
    let xmin = float (i mod nxsub) *. subw
    and ymin = float (nysub - 1 - i / nxsub) *. subh in
    {
      subpage_xmin = xmin;
      subpage_ymin = ymin;
      set_subpage = sprintf "set origin %.15g,%.15g;\n" xmin ymin;
      redraw = false;
      xmin = neg_infinity;  xmax = infinity;
      ymin = neg_infinity;  ymax = infinity;
      x1 = make_axis();
      x2 = make_axis();
      y1 = make_axis();
      y2 = make_axis();
      tics_outwards = false;
      title = "\"\"";     (* empty title <==> no title *)
      title_nlines = 0;
      title_lt = -1;  (* color of border *)
      title_color = -1; (* invalid color (as long as "lt" is possible) *)
      title_shown = false;
      title_old = ""; (* no previous title *)
      text_number = 1; (* The lower "label tag" number accepted by gnuplot *)
      tags = Tags.add system_tag { state = Managed;
                                   plots = Queue.create();
                                   texts = Queue.create();
                                 } Tags.empty; (* default queue only *)
      plot3D = false;
      zmin = neg_infinity;  zmax = infinity;
      z1 = make_axis();
      z2 = make_axis();
    } in
  let handle =
    {
      to_gplot = gp;
      gp_extra = extra;
      closed = false;
      interactive = (dev = X || dev = Wxt);
      max_inline = if offline then max_int else max_inline;
      buf_inline = Buffer.create (max_inline * 34);
      newpage = "unset multiplot;\n" ^ begin_new_page;
      (* subpages *)
      nxsub = nxsub; nysub = nysub; subw = subw; subh = subh;
      sub = 0;
      subpage = Array.init nsub make_subpage;
      (* Pen & fonts defaults *)
      pen = -1; (* border color *)
      color = -1; (* invalid color, must be >=0 to be active *)
      pen_width = 1.;
      point = 1;  point_width = 1.;
      font = default_font;
      font_size = 0; (* ==> default *)
    } in
  (* Initialize 1st page *)
  output_string gp begin_new_page;
  (* Prepare the 1st subpage if interactive *)
  if handle.interactive then begin
    moveto_subpage gp handle.subpage.(handle.sub);
    flush gp;
  end;
  (* Return handle *)
  handle


let close g =
  if not g.closed then begin
    g.closed <- true;
    if g.interactive then
      update_show g
    else
      (* Issue the plotting commands for the last page now *)
      Array.iter (fun sp ->
                    moveto_subpage g.to_gplot sp;
                    show_plots_all_tags
                      (if sp.plot3D then "splot " else "plot ")
                      g.to_gplot sp.tags) g.subpage;
    output_string g.to_gplot "unset multiplot;\n";
    flush g.to_gplot;
    try
      Gnuplot_sys.close_out g.to_gplot g.gp_extra
        (* This also removes the temp dir containing the data files. *)
    with Sys_error _ -> failwith "Gnuplot.close"
  end


let adv ?sub g =
  if g.closed then failwith "Gnuplot.adv";
  let nsub = g.nxsub * g.nysub in
  let sub = match sub with
    | None -> g.sub + 1
    | Some i -> if i < 1 then nsub else i - 1 in
  if sub >= nsub then begin
    (* Start a fresh new page at the first subpage *)
    if not g.interactive then
      (* Issue the plotting commands for the current page now *)
      Array.iter (fun sp ->
                    moveto_subpage g.to_gplot sp;
                    show_plots_all_tags
                      (if sp.plot3D then "splot " else "plot ")
                      g.to_gplot sp.tags) g.subpage;
    output_string g.to_gplot g.newpage;
    (* We start with a blank page, so everything is erased. *)
    (* FIXME: restore instead to default (init) state? *)
    Array.iter (fun sp ->
                  sp.x1.label_old <- "";
                  sp.x2.label_old <- "";
                  sp.y1.label_old <- "";
                  sp.y2.label_old <- "";
                  sp.z1.label_old <- "";
                  sp.title_old <- "";
                  Tags.iter (fun _ q -> match q.state with
                             | Managed | To_hide ->
                                 empty_plot_queue q;
                                 q.state <- Hidden
                                   (* FIXME: One may want to replot
                                      tagged plots so keep them ? *)
                             | _ -> ()) sp.tags;
               ) g.subpage;
    g.sub <- 0;
  end else
    g.sub <- sub;
  if g.interactive then
    (* Go to the subpage [sub] and re-set its parameters. *)
    moveto_subpage g.to_gplot g.subpage.(g.sub)


let clear g =
  if g.closed then failwith "Gnuplot.clear";
  if g.interactive then begin
    output_string g.to_gplot "clear;\n";
    flush g.to_gplot
  end;
  let sp = g.subpage.(g.sub) in
  (* All plots in the system_queue are destroyed. *)
  let q = Tags.find system_tag sp.tags in
  empty_plot_queue q;
  (* Update [To_hide] status to [Hidden] *)
  Tags.iter (fun _ q -> match q.state with
             | To_hide ->
                 q.state <- Hidden
             | _ -> ()) sp.tags



(*
 * Tag management
 **********************************************************************)

let tag_manage g fn_name ?tag immediately ~state =
  if g.closed then failwith fn_name;
  begin match tag with
  | None -> ()
  | Some t ->
      if t = system_tag then
        invalid_arg (fn_name ^ ": cannot change the system tag");
      (* Set the tag to the proper To_* state *)
      let sp = g.subpage.(g.sub) in
      try
        let q = Tags.find t sp.tags in
        q.state <- state
      with Not_found ->
        sp.tags <- Tags.add t { state = state;
                                plots = Queue.create();
                                texts = Queue.create();
                              } sp.tags
  end;
  (* Update display -- and possibly process the given tag *)
  if immediately then update_show g


let show ?(immediately=true) ?tag g =
  tag_manage g "Gnuplot.show" ?tag immediately ~state:To_show

let hide ?(immediately=false) ?tag g =
  tag_manage g "Gnuplot.hide" ?tag immediately ~state:To_hide

let auto ~tag g =
  tag_manage g "Gnuplot.auto" ~tag false ~state:Managed

let free ~tag g =
  if g.closed then failwith "Gnuplot.free";
  if tag = system_tag then
    invalid_arg "Gnuplot.free: cannot change the system tag";
  try
    let q = Tags.find tag g.subpage.(g.sub).tags in
    empty_plot_queue q
  with Not_found -> () (* no tag [t], ignore *)


(*
 * Pens, colors and fonts
 **********************************************************************)

let pen g i =
  if g.closed then failwith "Gnuplot.pen: handle closed";
  g.pen <- i

let color g c =
  if g.closed then failwith "Gnuplot.color: handle closed";
  if c < 0 || c > 0xFFFFFF then invalid_arg "Gnuplot.color";
  g.color <- c

let pen_width g w =
  if g.closed then failwith "Gnuplot.pen_width";
  g.pen_width <- w

let point g i =
    if g.closed then failwith "Gnuplot.point";
  g.point <- i

let point_width g w =
  if g.closed then failwith "Gnuplot.point_width";
  g.point_width <- w

let font g fn =
  if g.closed then failwith "Gnuplot.font";
  g.font <- fn

let font_size g sz =
  if g.closed then failwith "Gnuplot.font_size";
  g.font_size <- sz


(*
 * Text
 **********************************************************************)

(* [number_of_lines s] returns the number of lines of [s] as seen by
   Gnuplot for the positioning of the box.  Here are the (a bit odd)
   Gnuplot rules for this.

   -- Any number of "\n" at the beginning is ignored for the frame but
   pushes the title down (possibly in the plot area);

   -- After the first character, all "\n" count, two consecutive "\n"
   or an "\n" as last character imply an empty line;

   -- "" (and thus "\n\n...\n") is equivalent to no title, so 0 lines.

   The rule is therefore the following: ignore newlines at beginning,
   if what remains is "" this count as 0 lines, otherwise you got 1
   line and add one for each newline that follows.
*)
let number_of_lines =
  let rec skip_nl s i =
    if i >= String.length s || String.unsafe_get s i <> '\n' then i
    else skip_nl s (i + 1) in
  let rec count_nl s i acc =
    if i >= String.length s then acc else
      let acc = if String.unsafe_get s i = '\n' then acc + 1 else acc in
      count_nl s (i + 1) acc in
  fun s ->
    let i0 = skip_nl s 0 in
    if i0 = String.length s then 0 else count_nl s (i0 + 1) 1


let change_label g a1 a2 new_label =
  let change a =
    if a.label_shown then begin
      a.label_old <- a.label;
      a.label_shown <- false;
    end;
    let new_nlines = number_of_lines new_label in
    if new_nlines <> a.label_nlines then g.subpage.(g.sub).redraw <- true;
    a.label <- sprintf "%S font \"%s\"" new_label (string_of_font g);
    a.label_lt <- g.pen;
    a.label_color <- g.color;
    a.label_nlines <- new_nlines in
  match a1.active, a2.active with
  | false, true -> change a2
  | _ -> change a1

let xlabel g new_label =
  if g.closed then failwith "Gnuplot.xlabel: handle closed";
  let sp = g.subpage.(g.sub) in
  change_label g sp.x1 sp.x2 new_label

let ylabel g new_label =
  if g.closed then failwith "Gnuplot.ylabel: handle closed";
  let sp = g.subpage.(g.sub) in
  change_label g sp.y1 sp.y2 new_label

let title g new_title =
  if g.closed then failwith "Gnuplot.title: handle closed";
  let sp = g.subpage.(g.sub) in
  if sp.title_shown then begin
    sp.title_old <- sp.title;
    sp.title_shown <- false;
  end;
  let new_nlines = number_of_lines new_title in
  if new_nlines <> sp.title_nlines then sp.redraw <- true;
  sp.title <- sprintf "%S font \"%s\"" new_title (string_of_font g);
  sp.title_lt <- g.pen;
  sp.title_color <- g.color;
  sp.title_nlines <- new_nlines


type coord = Graph | Viewport | Subpage | World

(* FIXME: allow to choose which coordinates *)
(* FIXME: text is the same as a plot. same treatment. *)
let text g ?(tag=system_tag) ?(frame=0.) ?(rotate=0.) ?(coord=Graph) x y s =
  if g.closed then failwith "Gnuplot.text";
  let sp = g.subpage.(g.sub) in
  let pos = match coord with
    | Graph ->
        (* Use the active coordinate system *)
        sprintf "%s %F,%s %F"
          (if sp.x2.active && not sp.x1.active then "second" else "first") x
          (if sp.y2.active && not sp.y1.active then "second" else "first") y
    | Viewport -> sprintf "graph %F,graph %F" x y
    | Subpage ->
        sprintf "screen %F,screen %F"
          (sp.subpage_xmin +. x *. g.subw) (sp.subpage_ymin +. y *. g.subh)
    | World -> sprintf "screen %F,screen %F" x y in
  let text =
    sprintf "set label %i %S at %s rotate by %F font \"%s\" tc %s;\n"
      sp.text_number s pos rotate (string_of_font g)
      (if g.color >= 0 then sprintf "rgb \"#%X\"" g.color
       else sprintf "lt %i" g.pen) in
  let q = add_text_with_tag sp tag (text, sp.text_number) in
  begin match q.state with
  | Managed | Visible -> output_string g.to_gplot text;
  | _ -> ()
  end;
  sp.text_number <- sp.text_number + 1


(*
 * 2D: world coordinates, axes,...
 **********************************************************************)

(* FIXME: aspect ratio? here? *)
(* FIXME: What about aspect ratio?  Do we try to detect it
   automatically or let it be chosen by the user? *)
let win g xmin xmax ymin ymax =
  if g.closed then failwith "Gnuplot.win";
  let sp = g.subpage.(g.sub) in
  sp.xmin <- xmin;
  sp.xmax <- xmax;
  sp.ymin <- ymin;
  sp.ymax <- ymax;
  sp.redraw <- true (* schedule for redrawing which will reissue ranges *)
(*   let aspect = abs_float( ((ymax -. ymin) *. g.subw) *)
(*                           /. ((xmax -. xmin)*. g.subh) ) in *)
(*   if 0.95 <= aspect && aspect <= 1.05 then *)
(*     cmd_issue_and_hist g p "set size ratio -1\n"; (\* FIXME: not ideal *\) *)

type axis_opt =
    handle -> subpage -> float -> float -> axis -> axis -> unit
type border_loc = int list

let axis ?(which=[1]) () _ _ _ _ a1 a2 =
  if List.mem 1 which then a1.zero <- true;
  if List.mem 2 which then a2.zero <- true

let border ?(which=[1;2]) () _ _ _ _ a1 a2 =
  if List.mem 1 which then a1.draw <- true;
  if List.mem 2 which then a2.draw <- true;
  if List.mem 3 which then a1.draw_top <- true;
  if List.mem 4 which then a2.draw_top <- true

let tics ?(which=[1;2]) ?(outward=false) ?(grid=false) ?(minor=0)
  ?(minor_grid=false) ?(log=false) ?(step=0.) () =
  let set a min max =
      a.min <- min;
      a.max <- max;
      a.tics <- true;
      a.tics_step <- step; (* 0. = auto *)
      a.mtics <- minor;
      a.grid <- grid;
      a.mgrid <- minor_grid;
      if log && (min <= 0. || max <= 0.) then
        invalid_arg "Gnuplot.tics";
      a.logscale <- log  in
  fun _ sp min max a1 a2 ->
    if List.mem 1 which then set a1 min max;
    if List.mem 2 which then set a2 min max;
    sp.tics_outwards <- outward

(* FIXME: allows an easy way to put our own label, e.g. dates. *)
let labels ?(which=[1]) ?prec ?(rotate=false) () g _ _ _ a1 a2 =
  let font = string_of_font g in
  let set a =
    a.format <- (match prec with
                 | None -> "%g"
                 | Some p -> "%." ^ (string_of_int p) ^ "g");
    a.format_rot <- rotate;
    a.format_font <- font;
    a.format_lt <- g.pen;
    a.format_color <- g.color;
    a.active <- true in
  if List.mem 1 which then set a1 else a1.active <- false;
  if List.mem 2 which then set a2 else a2.active <- false


let default_opt = [border(); tics(); labels()]

(* REMARK: The default linetype is 1.  When axis labels are given this
   color, gnuplot will in fact display them with border color.  This
   is a bit strange but does not really interfere -- may be surprising
   to the user though.

   [box] cannot reset the axes to a default state before applying the
   options because we want incremental changes to be possible.

   FIXME: need a way to reset these parameters (e.g. labels, once set
   cannot be unset).  Do it in [clear] ? *)
let box ?(x=default_opt) ?(y=default_opt) g =
  if g.closed then failwith "Gnuplot.box";
  let sp = g.subpage.(g.sub) in
  List.iter (fun prop -> prop g sp sp.xmin sp.xmax sp.x1 sp.x2) x;
  List.iter (fun prop -> prop g sp sp.ymin sp.ymax sp.y1 sp.y2) y;
  sp.redraw <- true (* only matters for interactive mode *)


let env g ?(xaxis=false) ?(xgrid=false) ?(xlog=false) xmin xmax
  ?(yaxis=false) ?(ygrid=false) ?(ylog=false) ymin ymax =
  if g.closed then failwith "Gnuplot.env";
  win g xmin xmax ymin ymax;
  let x = [border(); tics ~grid:xgrid ~log:xlog (); labels()] in
  let x = if xaxis then axis() :: x else x in
  let y = [border(); tics ~grid:ygrid ~log:ylog (); labels()] in
  let y = if yaxis then axis() :: y else y in
  box g ~x ~y



(*
 * Generic plotting functions
 **********************************************************************)

type style = Lines | Linespoints | Points | Dots | Impulses

let with_style g = function
  | Lines -> "with lines"
  | Linespoints ->
      sprintf "with linespoints pt %i ps %F" g.point g.point_width
  | Points ->
      sprintf "with points pt %i ps %g" g.point g.point_width
  | Dots -> "with dots"
  | Impulses -> "with impulses"

let with_vector = "with vector"

(* Active axes for 'plot' *)
let axes g =
  let sp = g.subpage.(g.sub) in
  "axes " ^
  (if sp.x2.active && not sp.x1.active then "x2" else "x1") ^
  (if sp.y2.active && not sp.y1.active then "y2 " else "y1 ")

(* 'splot' does not support 'axes' *)
let no_axes _ = ""

(* REMARK: the callback, calling [loop] with the output command as
   argument, is even a bit faster than the loop with this command
   inlined! *)
let plot_data g ?(tag=system_tag) ~inline_if loop ~axes ~label ~style =
  (* The border linetype (-1) is twice as thick as the other ones.  So
     scale the size appropriately for visual homogeneity.  *)
  let lw = (if g.color < 0 && g.pen = -1 then
              0.5 *. g.pen_width else g.pen_width) in
  let plot =
    if inline_if then begin
      Buffer.reset g.buf_inline;
      try
        let () = loop(Buffer.add_string g.buf_inline) in
        Buffer.add_string g.buf_inline "e\n";
        let data = Buffer.contents g.buf_inline in
        (* Plot command *)
        let cmd = sprintf "'-' %s title %S %s" (axes g) label style in
        (cmd, lw, g.pen, g.color, Inlined(data))
      with e ->
        Buffer.reset g.buf_inline;
        raise e
    end
    else begin
      let (fname, fh) = Gnuplot_sys.open_temp_file g.gp_extra in
      try
        loop(output_string fh);
        close_out fh;
        (* Plot command *)
        let cmd =
          sprintf "%S %s title %S %s" fname (axes g) label style in
        (cmd, lw, g.pen, g.color, File fname)
      with e ->
        close_out fh;
        (try Unix.unlink fname with _ -> ());
        raise e
    end in
  (* Add plot and possibly display it *)
  update_show g ~add_plot:(tag, plot)



(*
 * 2D Plots
 **********************************************************************)

let fx g ?tag ?(style=Lines) ?(label="") ?(nsamples=100) f a b =
  if g.closed then failwith "Gnuplot.fx";
  if not(is_finite a) || not(is_finite b) || nsamples <= 1 then
    invalid_arg "Gnuplot.fx";
  let loop out =
    let nsamples = nsamples - 1 in
    let h = (b -. a) /. float nsamples in
    for i = 0 to nsamples do
      let x = a +. float i *. h in
      let y = f x in
      if is_finite y then
        out(string_of_float x ^ " " ^ string_of_float y ^ "\n")
      else
        out "? ?\n"
    done in
  plot_data g ~inline_if:(nsamples <= g.max_inline)
    loop ?tag ~axes ~label ~style:(with_style g style)



let xy_param g ?tag ?(style=Lines) ?(label="") ?(nsamples=100) f a b =
  if g.closed then failwith "Gnuplot.xy_param";
  if nsamples <= 1 then invalid_arg "Gnuplot.xy_param";
  let loop out =
    let nsamples = nsamples - 1 in
    let h = (b -. a) /. float nsamples in
    for i = 0 to nsamples do
      let x = a +. float i *. h in
      let (y1, y2) = f x in
      if is_finite y1 && is_finite y2 then begin
        out(string_of_float y1 ^ " " ^ string_of_float y2 ^ "\n")
      end else
        out "? ?\n"
    done in
  plot_data g ~inline_if:(nsamples <= g.max_inline)
    loop ?tag ~axes ~label ~style:(with_style g style)


(* This is similar to the second case of [plot_data] except the file
   is already created. *)
let xy_file g ?(tag=system_tag) ?(style=Lines) ?(label="") fname =
  if g.closed then failwith "Gnuplot.xy_file";
  let lw = if g.pen = -1 then 0.5 *. g.pen_width else g.pen_width in
  let cmd = sprintf "%S title %S %s %s"
              fname label (axes g) (with_style g style) in
  update_show g ~add_plot:(tag, (cmd, lw, g.pen, g.color, File fname))


(*
 * 3D: world coordinates, axes,...
 **********************************************************************)
(* FIXME: 3D plots are in alpha state *)

let win3 g xmin xmax ymin ymax zmin zmax =
  if g.closed then failwith "Gnuplot.win3";
  let sp = g.subpage.(g.sub) in
  sp.xmin <- xmin;
  sp.xmax <- xmax;
  sp.ymin <- ymin;
  sp.ymax <- ymax;
  sp.zmin <- zmin;
  sp.zmax <- zmax;
  sp.plot3D <- true;
  sp.redraw <- true

let default_opt3d = [border(); tics ~minor:0 (); labels()]
let default_opt3d_z = [border ~which:[1] ();
                       tics ~which:[1] ~minor:0 ();
                       labels()]

let box3 ?(x=default_opt3d) ?(y=default_opt3d) ?(z=default_opt3d_z) g =
  if g.closed then failwith "Gnuplot.box3";
  let sp = g.subpage.(g.sub) in
  List.iter (fun prop -> prop g sp sp.xmin sp.xmax sp.x1 sp.x2) x;
  List.iter (fun prop -> prop g sp sp.ymin sp.ymax sp.y1 sp.y2) y;
  List.iter (fun prop -> prop g sp sp.zmin sp.zmax sp.z1 sp.z2) z;
  sp.plot3D <- true;
  sp.redraw <- true

let env3 g ?(xaxis=false) ?(xgrid=false) ?(xlog=false) xmin xmax
  ?(yaxis=false) ?(ygrid=false) ?(ylog=false) ymin ymax
  ?(zaxis=false) ?(zgrid=false) ?(zlog=false) zmin zmax =
  if g.closed then failwith "Gnuplot.env3";
  win3 g xmin xmax ymin ymax zmin zmax;
  let x = [border(); tics ~minor:0 ~grid:xgrid ~log:xlog (); labels()] in
  let x = if xaxis then axis() :: x else x in
  let y = [border(); tics ~minor:0 ~grid:ygrid ~log:ylog (); labels()] in
  let y = if yaxis then axis() :: y else y in
  let z = [border ~which:[1] ();
           tics ~which:[1] ~minor:0 ~grid:ygrid ~log:ylog (); labels()] in
  let z = if zaxis then axis() :: z else z in
  box3 g ~x ~y ~z


(*
 * 3D Plots
 **********************************************************************)

(* FIXME: hidden -- global? *)
let fxy g ?tag ?(style=Lines) ?(label="")
  ?(xnsamples=30) ?(ynsamples=30) f xmin xmax ymin ymax =
  if g.closed then failwith "Gnuplot.fxy";
  if not(is_finite xmin && is_finite xmax && is_finite ymin
         && is_finite ymax) || xnsamples <= 1 || ynsamples <= 1 then
    invalid_arg "Gnuplot.fxy";
  let loop out =
    let xnsamples = xnsamples - 1
    and ynsamples = ynsamples - 1 in
    let hx = (xmax -. xmin) /. float xnsamples
    and hy = (ymax -. ymin) /. float ynsamples in
    for i = 0 to xnsamples do
      let x = xmin +. float i *. hx in
      for j = 0 to ynsamples do
        let y = ymin +. float j *. hy in
        let z = f x y in
        if is_finite z then out (sprintf "%F %F %F\n" x y z)
        else out (sprintf "%F %F ?\n" x y)
      done;
      out "\n";
    done in
  plot_data g ~inline_if:(xnsamples * ynsamples <= g.max_inline)
    loop ?tag ~axes:no_axes ~label ~style:(with_style g style)

let fxy_param g ?tag ?(style=Lines) ?(label="")
  ?(xnsamples=30) ?(ynsamples=30) f xmin xmax ymin ymax =
  if g.closed then failwith "Gnuplot.fxy_param";
  if xnsamples <= 1 || ynsamples <= 1 then invalid_arg "Gnuplot.fxy_param";
  let loop out =
    let xnsamples = xnsamples - 1
    and ynsamples = ynsamples - 1 in
    let hx = (xmax -. xmin) /. float xnsamples
    and hy = (ymax -. ymin) /. float ynsamples in
    for i = 0 to xnsamples do
      let x = xmin +. float i *. hx in
      for j = 0 to ynsamples do
        let y = ymin +. float j *. hy in
        let (z1, z2, z3) = f x y in
        if is_finite z1 && is_finite z2 && is_finite z3 then
          out (sprintf "%F %F %F\n" z1 z2 z3)
        else out "? ? ?\n"
      done;
      out "\n";
    done in
  plot_data g ~inline_if:(xnsamples * ynsamples <= g.max_inline)
    loop ?tag ~axes:no_axes ~label ~style:(with_style g style)


let xyz_ft g ?tag ?(style=Lines) ?(label="") ?(tnsamples=100) f tmin tmax =
  if g.closed then failwith "Gnuplot.xyz_ft";
  if tnsamples <= 1 then invalid_arg "Gnuplot.xyz_ft";
  let loop out =
    let tnsamples = tnsamples - 1 in
    let ht = (tmax -. tmin) /. float tnsamples in
    for i = 0 to tnsamples do
      let (z1, z2, z3) = f(tmin +. float i *. ht) in
      if is_finite z1 && is_finite z2 && is_finite z3 then
        out (sprintf "%F %F %F\n" z1 z2 z3)
      else out "? ? ?\n"
    done in
  plot_data g ~inline_if:(tnsamples <= g.max_inline)
    loop ?tag ~axes:no_axes ~label ~style:(with_style g style)

