(***********************************************************************)
(*                                                                     *)
(*                 MLTk, Tcl/Tk interface of OCaml                     *)
(*                                                                     *)
(*    Francois Rouaix, Francois Pessaux, Jun Furuse and Pierre Weis    *)
(*               projet Cristal, INRIA Rocquencourt                    *)
(*            Jacques Garrigue, Kyoto University RIMS                  *)
(*                                                                     *)
(*  Copyright 2002 Institut National de Recherche en Informatique et   *)
(*  en Automatique and Kyoto University.  All rights reserved.         *)
(*  This file is distributed under the terms of the GNU Library        *)
(*  General Public License, with the special exception on linking      *)
(*  described in file LICENSE found in the OCaml source tree.          *)
(*                                                                     *)
(***********************************************************************)

(* $Id$ *)

open Widget

type callback_buffer = string list
      (* Buffer for reading callback arguments *)

type tkArgs =
    TkToken of string
  | TkTokenList of tkArgs list          (* to be expanded *)
  | TkQuote of tkArgs                   (* mapped to Tcl list *)

type cbid = int

external opentk_low : string list -> unit
        =  "camltk_opentk"
external tcl_eval : string -> string
        =  "camltk_tcl_eval"
external tk_mainloop : unit -> unit
        =  "camltk_tk_mainloop"
external tcl_direct_eval : tkArgs array -> string
        =  "camltk_tcl_direct_eval"
external splitlist : string -> string list
        = "camltk_splitlist"
external tkreturn : string -> unit
        = "camltk_return"
external callback_init : unit -> unit
        = "camltk_init"
external finalizeTk : unit -> unit
        = "camltk_finalize"
    (* Finalize tcl/tk before exiting. This function will be automatically
       called when you call [Pervasives.exit ()] (This is installed at
       [install_cleanup ()] *)

let tcl_command s = ignore (tcl_eval s);;

type event_flag =
  DONT_WAIT | X_EVENTS | FILE_EVENTS | TIMER_EVENTS | IDLE_EVENTS | ALL_EVENTS
external do_one_event : event_flag list -> bool = "camltk_dooneevent"

let do_pending () = while do_one_event [DONT_WAIT] do () done

exception TkError of string
      (* Raised by the communication functions *)
let () = Callback.register_exception "tkerror" (TkError "")

let cltclinterp = ref Nativeint.zero
      (* For use in other extensions *)
let () = Callback.register "cltclinterp" cltclinterp

(* Debugging support *)
let debug =
 ref (try ignore (Sys.getenv "CAMLTKDEBUG"); true
      with Not_found -> false)

(* This is approximative, since we don't quote what needs to be quoted *)
let dump_args args =
  let rec print_arg = function
    TkToken s -> prerr_string s; prerr_string " "
  | TkTokenList l -> List.iter print_arg l
  | TkQuote a -> prerr_string "{"; print_arg a; prerr_string "} "
 in
  Array.iter print_arg args;
  prerr_newline()

(*
 * Evaluating Tcl code
 *   debugging support should not affect performances...
 *)

let tkEval args =
  if !debug then dump_args args;
  let res = tcl_direct_eval args in
  if !debug then begin
    prerr_string "->>";
    prerr_endline res
    end;
  res

let tkCommand args = ignore (tkEval args)

(*
 * Callbacks
 *)

(* LablTk only *)
let cCAMLtoTKwidget w =
  (* Widget.check_class w table; (* with subtyping, it is redundant *) *)
  TkToken (Widget.name w)

let cTKtoCAMLwidget = function
   "" -> raise (Invalid_argument "cTKtoCAMLwidget")
 | s -> Widget.get_atom s

let callback_naming_table =
   (Hashtbl.create 401 : (int, callback_buffer -> unit) Hashtbl.t)

let callback_memo_table =
   (Hashtbl.create 401 : (any widget, int) Hashtbl.t)

let new_function_id =
  let counter = ref 0 in
  function () -> incr counter;  !counter

let string_of_cbid = string_of_int

(* Add a new callback, associated to widget w *)
(* The callback should be cleared when w is destroyed *)
let register_callback w ~callback:f =
  let id = new_function_id () in
    Hashtbl.add callback_naming_table id f;
    if (forget_type w) <> (forget_type Widget.dummy) then
      Hashtbl.add callback_memo_table (forget_type w) id;
    (string_of_cbid id)

let clear_callback id =
  Hashtbl.remove callback_naming_table id

(* Clear callbacks associated to a given widget *)
let remove_callbacks w =
  let w = forget_type w in
  let cb_ids = Hashtbl.find_all callback_memo_table w in
    List.iter clear_callback cb_ids;
    for i = 1 to List.length cb_ids do
      Hashtbl.remove callback_memo_table w
    done

(* Hand-coded callback for destroyed widgets
 * This may be extended by the application, or by other layers of Camltk.
 * Could use bind + of Tk, but I'd rather give an alternate mechanism so
 * that hooks can be set up at load time (i.e. before openTk)
 *)
let destroy_hooks = ref []
let add_destroy_hook f =
  destroy_hooks := f :: !destroy_hooks

let _ =
  add_destroy_hook (fun w -> remove_callbacks w; Widget.remove w)

let install_cleanup () =
  let call_destroy_hooks = function
      [wname] ->
        let w = cTKtoCAMLwidget wname in
         List.iter (fun f -> f w) !destroy_hooks
    | _ -> raise (TkError "bad cleanup callback") in
  let fid = new_function_id () in
  Hashtbl.add callback_naming_table fid call_destroy_hooks;
  (* setup general destroy callback *)
  tcl_command ("bind all <Destroy> {camlcb " ^ (string_of_cbid fid) ^" %W}");
  at_exit finalizeTk

let prerr_cbid id =
  prerr_string "camlcb "; prerr_int id

(* The callback dispatch function *)
let dispatch_callback id args =
  if !debug then begin
    prerr_cbid id;
    List.iter (fun x -> prerr_string " "; prerr_string x) args;
    prerr_newline()
    end;
  (Hashtbl.find callback_naming_table id) args;
  if !debug then prerr_endline "<<-"

let protected_dispatch id args =
  try
    dispatch_callback id args
  with e ->
    Printf.eprintf "Uncaught exception: %s\n" (Printexc.to_string e);
    flush stderr

let _ = Callback.register "camlcb" protected_dispatch

(* Make sure the C variables are initialised *)
let _ = callback_init ()

(* Different version of initialisation functions *)
let default_display_name = ref ""
let default_display () = !default_display_name

let camltk_argv = ref []

(* options for Arg.parse *)
let keywords = [
  "-display", Arg.String (fun s ->
    camltk_argv := "-display" :: s :: !camltk_argv),
    "<disp> : X server to contact (CamlTk)";
  "-colormap", Arg.String (fun s ->
    camltk_argv := "-colormap" :: s :: !camltk_argv),
    "<colormap> : colormap to use (CamlTk)";
  "-geometry", Arg.String (fun s ->
    camltk_argv := "-geometry" :: s :: !camltk_argv),
    "<geom> : size and position (CamlTk)";
  "-name", Arg.String (fun s ->
    camltk_argv := "-name" :: s :: !camltk_argv),
    "<name> : application class (CamlTk)";
  "-sync", Arg.Unit (fun () ->
    camltk_argv := "-sync" :: !camltk_argv),
    ": sync mode (CamlTk)";
  "-use", Arg.String (fun s ->
    camltk_argv := "-use" :: s :: !camltk_argv),
    "<id> : parent window id (CamlTk)";
  "-window", Arg.String (fun s ->
    camltk_argv := "-use" :: s :: !camltk_argv),
    "<id> : parent window id (CamlTk)";
  "-visual", Arg.String (fun s ->
    camltk_argv := "-visual" :: s :: !camltk_argv),
    "<visual> : visual to use (CamlTk)" ]

let opentk_with_args argv (* = [argv1;..;argvn] *) =
  (* argv must be command line for wish *)
  let argv0 = Sys.argv.(0) in
  let rec find_display = function
    | "-display" :: s :: xs -> s
    | "-colormap" :: s :: xs -> find_display xs
    | "-geometry" :: s :: xs -> find_display xs
    | "-name" :: s :: xs -> find_display xs
    | "-sync" :: xs -> find_display xs
    | "-use" :: s :: xs -> find_display xs
    | "-window" :: s :: xs -> find_display xs
    | "-visual" :: s :: xs -> find_display xs
    | "--" :: _ -> ""
    | _ :: xs -> find_display xs
    | [] -> ""
  in
  default_display_name := find_display argv;
  opentk_low (argv0 :: argv);
  install_cleanup();
  Widget.default_toplevel

let opentk () = opentk_with_args !camltk_argv;;

let openTkClass s = opentk_with_args ["-name"; s]
let openTkDisplayClass disp cl = opentk_with_args ["-display"; disp; "-name"; cl]

(*JPF CAMLTK/LABLTK? *)
let openTk ?(display = "") ?(clas = "LablTk") () =
  let dispopt =
    match display with
    | "" -> []
    | _ -> ["-display"; display]
  in
  opentk_with_args (dispopt @ ["-name"; clas])

(* Destroy all widgets, thus cleaning up table and exiting the loop *)
let closeTk () =
  tcl_command "destroy ."

let mainLoop =
  tk_mainloop


(* [register tclname f] makes [f] available from Tcl with
   name [tclname] *)
let register tclname ~callback =
  let s = register_callback Widget.default_toplevel ~callback in
    tcl_command (Printf.sprintf "proc %s {args} {eval {camlcb %s} $args}"
                             tclname s)
