(* $Id: netlog.ml 1691 2012-02-05 18:29:36Z gerd $ *)

open Printf

type level =
    [ `Emerg | `Alert | `Crit | `Err | `Warning | `Notice | `Info | `Debug ]

type logger =
    level -> string -> unit

let level_weight =
  function
    | `Emerg   -> 0
    | `Alert   -> 1
    | `Crit    -> 2
    | `Err     -> 3
    | `Warning -> 4
    | `Notice  -> 5
    | `Info    -> 6
    | `Debug   -> 7

let level_names =
  [| "emerg"; "alert"; "crit"; "err"; "warning"; "notice"; "info"; "debug" |]

let string_of_level lev =
  level_names.( level_weight lev )


let level_of_string s =
  let s = String.lowercase s in
  match s with
    | "emerg"   -> `Emerg
    | "alert"   -> `Alert
    | "crit"    -> `Crit
    | "err"     -> `Err
    | "warning" -> `Warning
    | "notice"  -> `Notice
    | "info"    -> `Info
    | "debug"   -> `Debug
    | _         -> failwith ("Unknown level: " ^ s)

let weekday =
  [| "Sun"; "Mon"; "Tue"; "Wed"; "Thu"; "Fri"; "Sat" |]

let month =
  [| "Jan"; "Feb"; "Mar"; "Apr"; "May"; "Jun";
     "Jul"; "Aug"; "Sep"; "Oct"; "Nov"; "Dec" |]

type format = [ `Std | `ISO ]

let rec ten_power n =
  if n<=0 then 1 else 10 * (ten_power (n-1))

let billion = 1_000_000_000

let format_timestamp (fmt:format) digits (s,ns) =
  let s0 = floor s in
  let ns0 = truncate ( (s -. s0) *. 1E9 ) in
  let ns1 = if ns0 >= billion - ns then (ns-billion)+ns0 else ns+ns0 in
  let s1 = if ns0 >= billion - ns then s0 +. 1.0 else s0 in
  let ns_string = 
    if digits > 0 then
      sprintf ".%0*d" digits (ns1 / ten_power(9-digits))
    else 
      "" in

  let t = Unix.localtime s1 in
  match fmt with
    | `Std ->
	sprintf
	  "%s %s %2d %02d:%02d:%02d%s %4d"
	  weekday.(t.Unix.tm_wday)
	  month.(t.Unix.tm_mon)
	  t.Unix.tm_mday
	  t.Unix.tm_hour
	  t.Unix.tm_min
	  t.Unix.tm_sec
	  ns_string
	  (1900 + t.Unix.tm_year)
    | `ISO ->
	sprintf
	  "%4d-%02d-%02d %02d:%02d:%02d%s"
	  (1900 + t.Unix.tm_year)
	  (t.Unix.tm_mon+1)
	  t.Unix.tm_mday
	  t.Unix.tm_hour
	  t.Unix.tm_min
	  t.Unix.tm_sec
	  ns_string

let current_formatter =
  ref(format_timestamp `Std 0)


let channel_logger ch max_lev lev msg = 
  if level_weight lev <= level_weight max_lev then (
    let (sec,ns) =
      try Netsys_posix.clock_gettime Netsys_posix.CLOCK_REALTIME
      with Invalid_argument _ ->
	(Unix.gettimeofday(), 0) in
    let s =   (* Netdate is unavailable here *)
      sprintf
	"[%s] [%s] %s%s\n"
	(!current_formatter (sec,ns))
	(string_of_level lev)
	( match lev with
	    | `Debug ->
		sprintf "[%d:%d] " 
		  (Unix.getpid())
		  (!Netsys_oothr.provider # self # id)
	    | _ -> ""
	)
	msg in
    output_string ch s;
    flush ch
  )
    

let current_logger =
  ref(channel_logger Pervasives.stderr `Debug)


let log lev msg =
  !current_logger lev msg

let logf level fmt =
  Printf.ksprintf (log level) fmt

module Debug = struct
  type dlogger =
      string -> string -> unit

  let fwd_dlogger mname msg =
    log `Debug (mname ^ ": " ^ msg)

  let null_dlogger _ _ = ()

  let current_dlogger =
    ref fwd_dlogger

  let log mname msg =
    !current_dlogger mname msg

  let logf mname fmt =
    Printf.ksprintf (log mname) fmt

  let registry = Hashtbl.create 11

  let register_module mname evar =
    Hashtbl.replace registry mname evar

  let set_module mname b =
    try
      let evar = Hashtbl.find registry mname in
      evar := b
    with Not_found -> ()

  let set_all_modules b =
    Hashtbl.iter
      (fun _ evar -> evar := b)
      registry

  let enable_module mname =
    set_module mname true

  let disable_module mname =
    set_module mname false

  let enable_all () =
    set_all_modules true

  let disable_all () =
    set_all_modules false

  let names() =
    List.sort
      compare
      (Hashtbl.fold (fun name _ acc -> name::acc) registry [])

  let mk_dlog mname enable msg =
    if !enable then
      log mname msg

  let mk_dlogr mname enable f =
    if !enable then
      log mname (f())

  external int64_of_file_descr : Unix.file_descr -> int64
    = "netsys_int64_of_file_descr"
    (* Also occurs in netsys_win32.ml! *)

  type serial = < > ;;

  let new_serial() = (object end)

  let enable_fd_tracking = ref false

  let fd_tab = Hashtbl.create 50
  let fd_tab_mutex = !Netsys_oothr.provider # create_mutex()

  let fd_string_1 ?(owner=false) ?(descr=false) fd =
    try
      let (owner_s, descr_s, sn_opt, anchor_entry) =
	Hashtbl.find fd_tab fd in
      sprintf "%Ld(%s%s%s)"
	(int64_of_file_descr fd)
	(if owner then owner_s else "")
	(if owner && descr then " - " else "")
	(if descr then descr_s else "")
    with
      | Not_found ->
	  sprintf "%Ld(?)" (int64_of_file_descr fd)


  let finalise_anchor r _ =
    r := true

  let tracker =
    "Netlog"

  let track_fd ?(update=false) ?anchor ?sn ~owner ~descr fd =
    let anchor_entry =
      match anchor with
	| None -> None
	| Some x ->
	    let r = ref false in
	    Gc.finalise (finalise_anchor r) x;
	    Some r
    in
    Netsys_oothr.serialize
      fd_tab_mutex
      (fun () ->
	 if update then (
	   let verbose =
	     if Hashtbl.mem fd_tab fd then (
	       let (_, _, old_sn_opt, _) = Hashtbl.find fd_tab fd in
	       if old_sn_opt <> None && old_sn_opt <> sn then (
		 logf tracker "WARNING track_fd: descriptor already tracked \
                               with different sn as %s"
		   (fd_string_1 ~owner:true ~descr:true fd);
		 true
	       )
	       else !enable_fd_tracking
	     )
	     else !enable_fd_tracking in
	   Hashtbl.replace fd_tab fd (owner, descr, sn, anchor_entry);
	   if verbose then
	     logf tracker "track_fd: updating tracked descriptor %s"
	       (fd_string_1 ~owner:true ~descr:true fd)
	 )
	 else (
	   let verbose =
	     if Hashtbl.mem fd_tab fd then (
	       logf tracker "WARNING track_fd: descriptor already tracked as %s"
		 (fd_string_1 ~owner:true ~descr:true fd);
	       true
	     ) 
	     else !enable_fd_tracking in
	   Hashtbl.replace fd_tab fd (owner, descr, sn, anchor_entry);
	   if verbose then
	     logf tracker "track_fd: tracking descriptor %s"
	       (fd_string_1 ~owner:true ~descr:true fd)
	 )
      )
      ()

  let release_fd ?sn ?(force=false) fd =
    Netsys_oothr.serialize
      fd_tab_mutex
      (fun () ->
	 try
	   let (_, _, old_sn_opt, _) = Hashtbl.find fd_tab fd in
	   let verbose =
	     if old_sn_opt <> None && old_sn_opt <> sn && not force then (
	       logf tracker "WARNING release_fd: Descriptor is tracked \
                             with unexpected sn as %s"
		 (fd_string_1 ~owner:true ~descr:true fd);
	       true
	     )
	     else !enable_fd_tracking in
	   if verbose then
	     logf tracker "release_fd: releasing descriptor %s"
	       (fd_string_1 ~owner:true ~descr:true fd);
	   Hashtbl.remove fd_tab fd;
	 with
	   | Not_found ->
	       if not force then
		 logf tracker "WARNING release_fd: no such descriptor %s"
		   (fd_string_1 fd)
      )
      ()

  let fd_string ?owner ?descr fd =
    Netsys_oothr.serialize
      fd_tab_mutex
      (fun () -> fd_string_1 ?owner ?descr fd)
      ()

  let fd_table () =
    Netsys_oothr.serialize
      fd_tab_mutex
      (fun () ->
	 let tab =
	   Hashtbl.fold
	     (fun fd (owner,descr,_,anchor_flag) acc ->
		let n = int64_of_file_descr fd in
		let line =
		  sprintf "%4Ld  %-15s %-15s %s"
		    n
		    owner
		    descr
		    (match anchor_flag with
		       | Some flag -> if !flag then "DEAD" else ""
		       | _ -> ""
		    ) in
		(n,line) :: acc
	     )
	     fd_tab
	     [] in
	 let tab' =
	   List.sort (fun (n1,_) (n2,_) -> Int64.compare n1 n2) tab in
	 List.map snd tab'
      )
      ()
    
end
