(* $Id: uq_gtk.ml 1989 2014-08-24 12:44:32Z gerd $ *)

open Equeue
open Unixqueue

#ifdef GTK1
  type event_id = GMain.Io.event_source
#else
  type event_id = GMain.Io.id
#endif

type gtk_file_handler_rec = 
    { gtk_fd : Unix.file_descr;
      mutable gtk_event_source_in  : (event_id * bool ref) option;
      mutable gtk_event_source_out : (event_id * bool ref) option;
      mutable gtk_event_source_pri : (event_id * bool ref) option;
      mutable gtk_event_source_err : (event_id * bool ref) option;
      mutable gtk_event_source_hup : (event_id * bool ref) option;
    }


type runner =
    event_system -> (unit -> unit) -> unit


class gtk_event_system ?(run : runner option) () =
object (self)
  inherit Unixqueue_select.select_based_event_system() as super

  val mutable gtk_attaching = false
  val mutable gtk_run_soon = false
  val mutable gtk_is_running = false

  val mutable gtk_last_timer = (None : GMain.Timeout.id option)

  val mutable gtk_last_file_handlers =
                (Hashtbl.create 1 :
		   (Unix.file_descr, gtk_file_handler_rec) Hashtbl.t)

  val mutable gtk_watch_tuple = ([], [], [], -1.0)

  method private gtk_attach ?(run_soon=false) () =
    (* Creates an idle callback to reschedule events the next time the
     * event loop is entered. This step can be omitted when this method
     * is called from a Unixqueue callback (it is ensured gtk_setup
     * will be invoked soon).
     *
     * run_soon: if true, the Unixqueue is run once from the idle
     * callback. This can be used to process additional, non-file events.
     *)
    if not gtk_is_running then (
      (* prerr_endline "ATTACH!"; *)
      if not gtk_attaching then (
	gtk_attaching <- true;
	( match gtk_last_timer with
	      Some th -> GMain.Timeout.remove th; gtk_last_timer <- None
	    | None    -> ()
	);
	gtk_last_timer <- Some (GMain.Timeout.add
				  ~ms:0
				  ~callback:
				  (fun () ->
				     self#gtk_setup();
				     gtk_run_soon && (
				       gtk_run_soon <- false;
				       self#gtk_safe_handler false ([],[],[]) ()
				     )
				  ));
      );
      gtk_run_soon <- gtk_run_soon || run_soon;
    )
    (* else prerr_endline "(no attach)"; *)


  method private gtk_setup() =
    let (infiles, outfiles, oobfiles, time) as watch_tuple = super#setup() in

    gtk_watch_tuple <- watch_tuple;

    let ht = Hashtbl.create 50 (* n *) in   (* 50 should be enough *)

    (* Fill ht, the new hash table of file handlers: *)
    List.iter
      (fun fd ->
	 Hashtbl.replace ht fd (true,false,false))
      infiles;
    
    List.iter
      (fun fd ->
	 let (i,_,_) =
	   try Hashtbl.find ht fd
	   with Not_found -> (false,true,false)
	 in
	 Hashtbl.replace ht fd (i,true,false))
      outfiles;
    
    List.iter
      (fun fd ->
	 let (i,o,_) =
	   try Hashtbl.find ht fd
	   with Not_found -> (false,false,true)
	 in
	 Hashtbl.replace ht fd (i,o,true))
      oobfiles;

    let dest_handler (gh, is_active) =
      is_active := false;
      #ifdef GTK1
        ignore(GMain.Io.remove_source gh);
      #else
        (* GTK2 *)
        ignore(GMain.Io.remove gh);
      #endif
    in

    (* Update GTK file handlers: *)
    Hashtbl.iter
      (fun fd (i,o,x) ->
	 let mk_handler cond il ol xl =
	   let is_active = ref true in
	   (* Note: prio=150 has slightly lower priority than resize/redraw
	    * operations, but higher priority than idle callbacks
	    *)
	   let gh =
	     GMain.Io.add_watch 
	       ~prio:150
	       ~cond:(
		 #ifdef GTK2_IO_ADD_WATCH_SUPPORTS_LISTS
		   [cond]
		 #else
		   cond
                 #endif
               )
	       ~callback:(
		 fun _ ->
		   !is_active &&
		     self#gtk_safe_handler true (il,ol,xl) ())
	       (GMain.Io.channel_of_descr fd) in
	   (gh, is_active) in
	 let g = 
	   try Hashtbl.find gtk_last_file_handlers fd 
	   with Not_found ->
	     { gtk_fd = fd;
	       gtk_event_source_in = None;
	       gtk_event_source_out = None;
	       gtk_event_source_pri = None;
	       gtk_event_source_err = None;
	       gtk_event_source_hup = None; } in
	 ( match g.gtk_event_source_in with
	       None when i ->
		 g.gtk_event_source_in <- Some(mk_handler Uq_gtk_helper._in [fd] [] []);
	     | Some s when not i ->
		 dest_handler s;
		 g.gtk_event_source_in <- None
	     | _ ->
		 ()
	 );
	 ( match g.gtk_event_source_out with
	       None when o ->
		 g.gtk_event_source_out <- Some(mk_handler `OUT [] [fd] []);
	     | Some s when not o ->
		 dest_handler s;
		 g.gtk_event_source_out <- None
	     | _ ->
		 ()
	 );
	 ( match g.gtk_event_source_pri with
	       None when x ->
		 g.gtk_event_source_pri <- Some(mk_handler `PRI [] [] [fd]);
	     | Some s when not x ->
		 dest_handler s;
		 g.gtk_event_source_pri <- None
	     | _ ->
		 ()
	 );
	 ( match g.gtk_event_source_err with
	       None when i || o || x ->
		 let il = if i then [fd] else [] in
		 let ol = if o then [fd] else [] in
		 let xl = if x then [fd] else [] in
		 g.gtk_event_source_err <- Some(mk_handler `ERR il ol xl);
	     | Some s when not (i || o || x) ->
		 dest_handler s;
		 g.gtk_event_source_err <- None
	     | _ ->
		 ()
	 );
	 ( match g.gtk_event_source_hup with
	       None when i || o || x ->
		 let il = if i then [fd] else [] in
		 let ol = if o then [fd] else [] in
		 let xl = if x then [fd] else [] in
		 g.gtk_event_source_hup <- Some(mk_handler `HUP il ol xl);
	     | Some s when not (i || o || x) ->
		 dest_handler s;
		 g.gtk_event_source_hup <- None
	     | _ ->
		 ()
	 );
	 Hashtbl.replace gtk_last_file_handlers fd g
      )
      ht;

    Hashtbl.iter
      (fun fd g ->
	 if not (Hashtbl.mem ht fd) then (
	   ( match g.gtk_event_source_in with
		 Some s ->
		   dest_handler s;
		   g.gtk_event_source_in <- None
	       | _ -> ()
	   );
	   ( match g.gtk_event_source_out with
		 Some s ->
		   dest_handler s;
		   g.gtk_event_source_out <- None
	       | _ -> ()
	   );
	   ( match g.gtk_event_source_pri with
		 Some s ->
		   dest_handler s;
		   g.gtk_event_source_pri <- None
	       | _ -> ()
	   );
	   ( match g.gtk_event_source_err with
		 Some s ->
		   dest_handler s;
		   g.gtk_event_source_err <- None
	       | _ -> ()
	   );
	   ( match g.gtk_event_source_hup with
		 Some s ->
		   dest_handler s;
		   g.gtk_event_source_hup <- None
	       | _ -> ()
	   );
	 )
      )
      gtk_last_file_handlers;

    let watching_files = infiles <> [] || 
			 outfiles <> [] || 
			 oobfiles <> [] in
    

    (* Remove the old timer, if any. *)
    begin match gtk_last_timer with
	None -> ()
      | Some th -> 
	  GMain.Timeout.remove th;
	  gtk_last_timer <- None;
    end;
    gtk_attaching <- false;

    (* Set the new timer, if necessary *)
    if time >= 0.0 then begin
      (* prerr_endline ("Timeout: " ^ string_of_float time); *)
      gtk_last_timer <- Some (GMain.Timeout.add
				~ms:(int_of_float (time *. 1E3 +. 0.5))
				~callback:(self#gtk_safe_handler false ([],[],[])));
    end;

    (* If no handler is active, detach. *)
    (*
    if gtk_last_timer = None && not watching_files then begin
      gtk_attached <- false;
      (* prerr_endline "Detached!"; *)
    end;
    *)

  method private gtk_safe_handler keep watch_tuple () =
    try
      self#gtk_handler watch_tuple ();
      keep
    with
	any ->
	  prerr_endline("Uq_gtk: Internal uncaught exception: " ^
			Netexn.to_string any);
	  raise any;
	  keep

  method private gtk_handler watch_tuple () =

    (* IMPORTANT:
     * It is possible that this is a "ghost event". We need to test whether
     * there is a resource for the event or not.
     *)

    (* Do now a 'select' with zero timeout to test the file descriptors. *)

    let (infiles,outfiles,oobfiles) = watch_tuple in

    let (infiles', outfiles', oobfiles') as actual_tuple =
      (* (infiles', outfiles', oobfiles'): Lists of file descriptors that
       * can be handled
       *)
      Unix.select infiles outfiles oobfiles 0.0
	(* Because of the timeout value 0.0, this "select" call cannot block,
	 * and it cannot raise EINTR.
	 *)
    in

    (* Now we have in infiles', outfiles', oobfiles' the actually happened
     * file descriptor events.
     * Furthermore, pure timeout events may have happened, but this is not
     * indicated specially.
     *)
    ignore(self#queue_events actual_tuple);

    (* Now run the queue (without source). *)
    begin try
      gtk_is_running <- true;
      match run with
	  None   -> super#run()
	| Some r -> r (self : #event_system :> event_system) super#run;
    with
	any ->
	  prerr_endline ("Uq_gtk: Uncaught exception: " ^
			 Netexn.to_string any
			);
    end;
    gtk_is_running <- false;
    
    (* Set up for the next round. *)
    self#gtk_setup ();

  (**********************************************************************)
  (* Overriden methods                                                  *)
  (**********************************************************************)
 
  method private source _ =
    (* Override this method: All events are coming from the glib loop,
     * so disable this source of events
     *)
    ()

  (* After certain method invocations, we must ensure we are attached: *)

  method add_resource g (op,t) =
    super # add_resource g (op,t);
    self # gtk_attach()

  method remove_resource g op =
    super # remove_resource g op;
    self # gtk_attach()

  method add_event e =
    super # add_event e;
    self # gtk_attach ~run_soon:true ()


  method run() =
    (* Calling this method is an error! *)
    failwith "gtk_event_system#run: This method is disabled. Run the Glib event loop instead!"

end
;;
