(***********************************************************************)
(*                                                                     *)
(*                             Active-DVI                              *)
(*                                                                     *)
(*                   Projet Cristal, INRIA Rocquencourt                *)
(*                                                                     *)
(*  Copyright 2002 Institut National de Recherche en Informatique et   *)
(*  en Automatique.  All rights reserved.  This file is distributed    *)
(*  under the terms of the GNU Lesser General Public License.          *)
(*                                                                     *)
(*  Jun Furuse, Didier Rémy and Pierre Weis.                           *)
(*  Contributions by Roberto Di Cosmo, Didier Le Botlan,               *)
(*  Xavier Leroy, and Alan Schmitt.                                    *)
(*                                                                     *)
(*  Based on Mldvi by Alexandre Miquel.                                *)
(***********************************************************************)

(* $Id: gs.ml,v 1.41 2004/07/16 07:51:35 weis Exp $ *)

let debugs = Misc.debug_endline;;

let get_do_ps, set_do_ps, init_do_ps =
 let has_to_do_ps = ref !Global_options.pson in
 (fun () -> !has_to_do_ps),
 (fun b -> has_to_do_ps := b),
 (fun () -> has_to_do_ps := !Global_options.pson);;

let antialias =
  Options.flag false
    "-A"
    "  ask Active-DVI to use PostScript antialiasing,\
    \n\t (the default is not to use PostScript antialiasing).";;

let pstricks =
  Options.flag false
    "-pstricks"
    "  ask Active-DVI to show moveto,\
    \n\t (the default is not to show moveto).";;

let showps_ref = ref false;;
let showps s =
  if !showps_ref then (print_endline  (Printf.sprintf "%s" s));;

(* (Printf.sprintf "SHOWPS: %s" s));;*)

Options.add
  "--showps" (Arg.Set showps_ref)
  "  ask advi to print to stdout a copy\
  \n\t of the PostScript program sent to gs.";;

let pspage = ref 0;;

(* constants *)
(** ack_string is an arbitrary string that would not be ``naturally''
   generated by gs. Xdvi used characters \347\310\376 in octet which is
   \231\200\254 in decimal.
   I don't know why... to test whether gs is new or old *)
let ack_string = "\231\200\254\n";;
let pos_string = "dvi";;
let err_string = "Error:";;
let current_x = ref 0;;
let current_y = ref 0;;

let parse_pos s =
  let c = String.index s ',' in
  let bc = s.[3] in
  (* y comes first, then x *)
  let y = String.sub s 3 (c - 3) in
  let x = String.sub s (c + 1) (String.length s - c - 1) in
  Misc.round (float_of_string x), Misc.round (float_of_string y);;

let ack_request =
  String.concat ""
    [ "flushpage ("; ack_string; ") print flush "; ];;

let timeout = 3.;;

exception Timeout;;
exception Error;;

exception Terminated;;
exception Retry;;

let x11alpha = "x11alpha";;
let x11 = "x11";;
type graphical =
    { display : int;
      window  : int32 (* GraphicsY11.window_id *) ; (* Window identifier. *)
      pixmap  : int32 (* GraphicsY11.window_id *) ; (* Pixmap identifier. *)
      width  : int ; (* geometry of the window. *)
      height : int ;
      bwidth  : int ; (* geometry of the backing store. *)
      bheight : int ;
      xdpi : float ; (* x resolution *)
      ydpi : float ; (* y resolution *)
      x    : int   ; (* x offset in pixels
                          (coordinates of the upper left corner) *)
      y    : int   ; (* y offset *)
    };;

exception Killed of string;;

let rec select fd_in fd_out fd_exn timeout =
  (* dirty hack: Graphics uses itimer internally! *)
  let start = Unix.gettimeofday () in
  try
    Unix.select fd_in fd_out fd_exn timeout
  with
    Unix.Unix_error (Unix.EINTR, _, _) as exn ->
      let now = Unix.gettimeofday () in
      let remaining = start +. timeout -. now in
      if remaining > 0.0 then select fd_in fd_out fd_exn timeout else [], [], []
;;

class gs () =
  let _ = GraphicsY11.window_id() in
  let gr = {
      display = 0;
      window = GraphicsY11.window_id ();
      pixmap = GraphicsY11.bstore_id ();
      width = Graphics.size_x ();
      height = Graphics.size_y ();
      bwidth = GraphicsY11.bsize_x ();
      bheight = GraphicsY11.bsize_y ();
      xdpi = 72.0;
      ydpi = 72.0;
      x = 0;
      y = 0;
    } in
  let dpi = 72 (* unite utilise par dvi? *) in
  let command = Config.gs_path in
  let command_args =
    [|
      command; 
      "-dNOPLATFONTS"; "-dNOPAUSE";
      "-sDEVICE=" ^ (if !antialias then x11alpha else x11);
      "-q";
      "-dSAFER";
      "-";
    |] in

  let _ = debugs command;
  Array.iter debugs command_args in

  (* Set environment so that ghostscript writes in our window. *)
  let _ =
    Unix.putenv "GHOSTVIEW"
      (if Global_options.get_global_display_mode ()
       then Printf.sprintf "%lu " gr.window
       else Printf.sprintf "%lu %lu " gr.window gr.pixmap) in

  let iof = Misc.round and foi = float_of_int in
  let lx = iof ( (foi (gr.x * dpi)) /. gr.xdpi)
  and ly = iof ( (foi (gr.y * dpi)) /. gr.ydpi)
  and ux = iof ( (foi ((gr.x + gr.bwidth)  * dpi)) /. gr.xdpi )
  and uy = iof ( (foi ((gr.y + gr.bheight) * dpi)) /. gr.ydpi) in

  (* Set ghostscript property. *)
  let content =
    Printf.sprintf "%s %d %d %d %d %d %f %f %d %d %d %d"
      "0" (* no backing pixmap for the window *)
      0   (* Rotation : 0 90 180 270 *)
      lx ly ux uy
      (* lower-left x y, upper-right x y :
         Bounding box in default user coordinates. *)
      gr.xdpi gr.ydpi (* Resolution x y. *)
      0 0 0 0 (* Margins left, bottom, top, right. *) in

  let _ =
    begin
      try GraphicsY11.set_named_atom_property  "GHOSTVIEW" content;
      with x -> Misc.fatal_error "Cannot set ``GHOSTVIEW'' property"
    end;
  (* Ignore signal SIGPIPE. *)
  Unix.sigprocmask Unix.SIG_BLOCK [ Sys.sigpipe ] in

  let lpd_in, lpd_out = Unix.pipe () in
  let rpd_in, rpd_out = Unix.pipe () in
  let leftout = Unix.out_channel_of_descr lpd_out in
  let rightin = Unix.in_channel_of_descr rpd_in in
  let close_all () =
    let tryc f x = try f x with _ -> () in
    tryc close_out leftout;
    tryc close_in rightin;
    tryc Unix.close lpd_in;
    tryc Unix.close rpd_in in
  let pid =
    Unix.create_process command command_args lpd_in rpd_out
      (* Unix.stdout *) Unix.stderr
  in
  object (self)
    val pid = pid
    val mutable ack = 0
    method gr = gr

    method ack_request =
      try
        ack <- ack + 1;
        self # line ack_request;
        flush leftout;
        self # ack;
      with
      | Killed s ->
          self # kill;
          Misc.warning s;
          raise Terminated
      | exn ->
          Misc.warning (Printexc.to_string exn);
          self # kill; 
          raise Terminated

    method ack =
      if ack > 0 then
        begin
          let s =
            try input_line rightin
            with End_of_file ->
              match select [ rpd_in ] [] [] 1.0 with
              | [], _, _ ->
                  begin match Unix.waitpid [ Unix.WNOHANG ] pid with
                  | x, Unix.WEXITED y when x > 0 ->
                      raise (Killed "gs exited")
                  | 0, _ ->
                      raise (Killed "gs alive but not responding")
                  | _, _ ->
                      raise (Killed "gs in strange state")
                  end
              | _, _, _ ->
                  input_line rightin in
          if Misc.has_prefix s ack_string then ack <- ack - 1 else
          if Misc.has_prefix pos_string s then
            begin
              try
                let x, y = parse_pos s in
                current_x := x; current_y := y
              with
              | Not_found | Failure _ -> Misc.warning s
            end else
          if Misc.has_prefix err_string s then
            begin
              Misc.warning s;
              raise (Killed "Error in Postscript");
            end else
          Misc.warning s;
          self # ack
        end;

    method kill =
      try
        Unix.kill pid Sys.sighup;  (* Sys.sigkill? *)
        let _, _ = Unix.waitpid [] pid in
        close_all ();
      with Unix.Unix_error (_, _, _) -> ()

    method line l =
      try
        showps l;
        output_string leftout l;
        output_char leftout '\n';
      with exc ->
        Misc.warning (Printexc.to_string exc);
        self # kill;
        Misc.warning "GS Terminated"

    method sync =
      try self # ack_request;
      with
        Killed s ->
          self # kill;
          set_do_ps false;
          Misc.warning (Printf.sprintf "%s\nContinuing without gs\n" s)

    method ps b =
      List.iter self#line b;

  end;;

let advi_pro =
"/advi@floatstring 20 string def
/advi@printfloat { advi@floatstring cvs print } def
/advi@CP {
CM [1 0 0 -1 0 0] setmatrix
    CP (dvi) print advi@printfloat  (,) print advi@printfloat (\n) print 
setmatrix } def";;
let advi_noshowpage = " /showpage { } /def"
let advi_resetmatrix = "[1 0 0 -1 0 0] concat"

type special = Unprotected | Protected | Begin | Continue | End 

let texbegin = "TeXDict begin";;
let texend = "flushpage end";;
let moveto x y =
  (*   current_x := x; current_y := y; *)
  Printf.sprintf "%d %d moveto" x y
;;

let make_header (b, h) =
  if b then
    String.concat "\n"
      [ "TeXDict begin @defspecial";
        h;
        "@fedspecial end"; ]
  else String.concat "" [ "("; h; ") run" ];;

let texc_special_pro gv =
  let l = [ "texc.pro"; "special.pro" ] in
  try
    let l' = Search.true_file_names [] l in
    if List.length l = List.length l' then
      List.map (fun s -> make_header (false, s)) l'
    else raise Not_found
  with
    Not_found ->
      Misc.warning "Cannot find Postscript prologues: texc.pro or special.pro";
      Misc.warning "Continuing without Postscript specials";
      gv # kill;
      set_do_ps false;
      [];;

class gv =
  object (self)
    val mutable dirtypage = true
    val mutable process : gs option = None
    val mutable dpi = 72
    val mutable mag = 1.
    val mutable xorig = 0
    val mutable yorig = 0
    val mutable sync = true
    val mutable headers = []
    method line =
      try
        self # process # line
      with Terminated ->
        process <- None;
        raise Terminated
    method moveto x y =
      let x' = x + xorig in
      let y' = y - yorig  in
      moveto x' y'

    method current_point =
      let x' = !current_x in
      let y' = !current_y + (self # process # gr).height in
      x', y'

    method check_size =
      begin
        match process with
        | None -> ()
        | Some gs ->
            let gr = gs # gr in
            let size_x = GraphicsY11.bsize_x () in
            let size_y = GraphicsY11.bsize_y () in
            if size_x <> gr.bwidth || size_y <> gr.bheight
            then self # kill;
      end;

    method sync =
      if not sync then
        begin
          match process with
          | Some p -> p # sync; sync <- true
          | None -> ()
        end

    method add_headers l =
      if headers = [] then headers <- texc_special_pro self;
      let l = List.map make_header l in
      match
        List.filter 
          (function s -> List.for_all (function s' -> s <> s') headers)
          l
      with
      | [] -> ()
      | l ->
          headers <- headers @ l;
          match process with
          | Some gs ->
              gs # line "grestore SI restore";
              List.iter (gs # line) l;
              (* to avoid no-current-point *)
              gs # line "0 0 moveto";
              gs # line "/SI save def gsave";
          | None -> ()

    method newpage l sdpi m x y =
      self # check_size;
      let l = List.map make_header l in
      let l = List.filter (fun s -> not (List.mem s headers)) l in
      let gs = self # process in
      if l <> [] then headers <- headers @ l;
      dpi <- sdpi;
      mag <- m;
      xorig <- x;
      yorig <- (gs # gr).height - y;
      if !pspage > 0 then showps "showpage";
      incr pspage;
      showps (Printf.sprintf "%%%%Page: %d %d\n" !pspage !pspage);
      gs # line "\n%% Newpage\n";
      gs # line "grestore";
      gs # line "0 0 moveto";
      if l <> [] then gs # line "SI restore";
      gs # line
        (Printf.sprintf
           "TeXDict begin %d %d div dup /Resolution X /VResolution X end"
           dpi dpi);
      gs # line
        (Printf.sprintf
           "TeXDict begin /DVImag %f def end"
           mag);
      List.iter gs # line l;
      if l <> [] then gs # line  "/SI save def";
      gs # line "gsave";
      gs # sync


    method process =
      match process with
      | None ->
          if not (get_do_ps ()) then raise Terminated;
          let gs = new gs () in
          if headers = [] then headers <-  texc_special_pro self;
          (* should take matrix as a parameter ? *)
          showps "%!PS-Adobe-2.0\n%%Creator: Active-DVI\n%!";
          gs # line "[1 0 0 -1 0 0] concat";
          List.iter (gs # line) headers;
          gs # line advi_pro;
          gs # line "TeXDict begin @landscape end";
          gs # line "/SI save def gsave";
          process <- Some gs;
          gs
      | Some gs ->
(*prerr_endline (Printf.sprintf "Calling existing gs interpreter");*)
          gs

    method send b  =
(*prerr_endline (Printf.sprintf "Calling gv#SEND with\n\t\t %s" (String.concat " " b));*)
      self # check_size;
      self # process # ps b;
      sync <- false

    method def (b : string) =
      (* insert into postscript into user dictionnary, typically with '!'
         should not change graphic parameters *)
      (* does not draw---no flushpage *)
      (* in fact b should have already be loaded through *)
      self # send [ ]

    method ps action b (x : int) (y : int) =
      (* prerr_endline (Printf.sprintf "Calling gv#PS with\n\t\t %s" b); *)
      self # send
        begin match action with
        | Unprotected ->
            (* insert non protected postscript, typically with ``ps:''
               may (not ?) change graphic parameters;
               still need to set current point for initclip PStrick specials.
             *)
            [ texbegin; self # moveto x y; b; texend ];
        | Protected ->
            (* insert protected postscript, typically with '"'
               does (?) change graphic parameters *)
            [ texbegin;
              self # moveto x y;
              "@beginspecial @setspecial";
              b;
              "@endspecial"; texend;
            ] ;
        | Begin ->
            (* Open SDict as @beginspecial
               but do not reset graphical parameters *)
            [ texbegin; 
              self # moveto x y;
              "@defspecial"; 
              b ];
        | Continue -> 
            (* Continue [in SDict] *)
            [ b ];
        | End -> 
            (* Close SDict *)
            [ "@fedspecial"; texend; ];
        end; 
      sync <- false

    method file name (llx, lly, urx, ury) (rwi, rhi : int * int) x y =
      (* insert into postscript into user dictionnary, typically with '!'
         should not change graphic parameters *)
      (* does not draw---no flushpage *)
      let ri z s = if z <> 0 then Printf.sprintf "%d %s" z s else "" in
      self # send [ texbegin;
                    self # moveto x y;
                    "@beginspecial";
                    Printf.sprintf "%d @llx %d @lly %d @urx %d @ury"
                      llx lly urx ury;
                    ri rwi "@rwi"; ri rhi "@rhi";
                    "@setspecial";
                    Printf.sprintf"(%s) run" name;
                    "@endspecial";
                    texend;
                  ] ;
      sync <- false

    method kill =
      showps "showpage";
      match process with
      | None -> ()
      | Some gs ->
          gs # kill;
          process <- None
  end
;;

let gv = new gv;;

(* exported functions *)

let kill () = gv # kill;;

let ps_forms =
  [ 
    "ps: ", Unprotected;
    "\" ", Protected;
    "ps::[begin]", Begin;
    "ps::[end]", End;
    "ps::", Continue;
    "ps:", Unprotected;
   ];;

let draw s x y =
  if get_do_ps () then
    let pred (prefix, action) =
      try gv#ps action (Misc.get_suffix prefix s) x y; true
      with Misc.Match -> false in
    if not (List.exists pred ps_forms) then
      try gv#ps Protected (Misc.get_suffix  "\" " s) x y with Misc.Match ->
        try gv#def (Misc.get_suffix  "! " s) with Misc.Match -> 
(*
   | Misc.Match ->
   try gv#file (Misc.get_suffix  "psfile: " s) bbox size x y with
 *)
          Misc.warning
            (Printf.sprintf "Unknown PostScript commands\n\t\t %s" s)
;;
let draw_file fname bbox ri x y =
  if get_do_ps () then gv#file fname bbox ri x y;;

let current_point() =
    gv#current_point;;

let add_headers x =
  gv#add_headers x;;

let newpage x =
  gv#newpage x;;

let flush () =
  if get_do_ps () then
    try  gv#sync
    with
    | Terminated ->
        Misc.warning "Continuing without Postscript";
        gv#kill;
        set_do_ps false;;

let toggle_antialiasing () =
  antialias := not !antialias;
  kill ();;

