(* $Id$ *)

open Printf

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

let level_weight = Netlog.level_weight
let level_of_string = Netlog.level_of_string
let string_of_level = Netlog.string_of_level
let level_names = Netlog.level_names


class type generic_config =
object
  method log_format : string
  method log_component : string
  method log_subchannel : string
  method log_max_level : level
end


let std_fmt =
  "[${timestamp}] [${component}] [${level}] ${message}"

class default_generic_config =
object
  method log_format = std_fmt
  method log_component = "*"
  method log_subchannel = "*"
  method log_max_level = (`Debug : level)
end


class type logger =
object
  method log_subch : component:string -> subchannel:string -> 
                     level:level -> message:string -> unit
  method log : component:string -> level:level -> message:string -> unit
  method reopen : unit -> unit
end


let format_message fmt component subchannel level message =
  let (sec,ns) =
    try Netsys_posix.clock_gettime Netsys_posix.CLOCK_REALTIME
    with Invalid_argument _ ->
      (Unix.gettimeofday(), 0) in
  let nd = lazy(Netdate.create ~localzone:true ~nanos:ns sec) in
  let b = Buffer.create 100 in
  Buffer.add_substitute
    b
    (fun var ->
       match var with
	 | "timestamp" ->
	     !Netlog.current_formatter (sec,ns)
	 | "timestamp:unix" ->
	     sprintf "%.0f" (Netdate.since_epoch (Lazy.force nd))
	 | "component" ->
	     component
	 | "subchannel" ->
	     subchannel
	 | "level" ->
	     string_of_level level
	 | "message" ->
	     message
	 | _ ->
	     if (String.length var >= 10 && 
		   String.sub var 0 10 = "timestamp:") then
	       let nd_fmt =
		 String.sub var 10 (String.length var - 10) in
	       Netdate.format nd_fmt (Lazy.force nd)
	     else
	       ""
    )
    fmt;
  Buffer.contents b


let ast_re = Netstring_str.regexp "[*]";;

let regexp_of_pattern s =
  let l = Netstring_str.split_delim ast_re s in
  Netstring_str.regexp
    (String.concat ".*" (List.map (fun u -> Netstring_str.quote u) l) ^ "$")


let mk_filter (gc:generic_config) =
  let comp_re = regexp_of_pattern gc#log_component in
  let subch_re = regexp_of_pattern gc#log_subchannel in

  fun component subchannel level ->
    level_weight level <= level_weight gc#log_max_level && (
      (Netstring_str.string_match comp_re component 0 <> None) &&
      (Netstring_str.string_match subch_re subchannel 0 <> None)
    )
  


class channel_logger (gc:generic_config) out : logger =
  let filter = mk_filter gc in
object(self)
  method log_subch ~component ~subchannel ~level ~message =
    if filter component subchannel level then (
      let fmt = gc#log_format in
      try
	fprintf out "%s\n%!"
	  (format_message fmt component subchannel level message)
      with
	| error ->
	    prerr_endline 
	      ("Netplex Catastrophic Error: Unable to write to log channel: " ^
		 Netexn.to_string error)
    )

  method log =
    self # log_subch ~subchannel:""

  method reopen() = ()

end

let channel_logger = new channel_logger (new default_generic_config)

let channel_logger_from_obj = new channel_logger


class extract_generic_config cf addr =
  let fmt = 
    try cf # string_param (cf # resolve_parameter addr "format")
    with Not_found -> std_fmt in
  let component =
    try cf # string_param (cf # resolve_parameter addr "component")
    with Not_found -> "*" in
  let subchannel =
    try cf # string_param (cf # resolve_parameter addr "subchannel")
    with Not_found -> "*" in
  let max_level_str =
    try  cf # string_param (cf # resolve_parameter addr "max_level")
    with Not_found -> "all" in
  let max_level =
    try
      if STRING_LOWERCASE (max_level_str) = "all" then
	`Debug
      else
	(level_of_string max_level_str)
    with
      | _ ->
	  failwith ("In section " ^ cf # print addr ^ 
		      ": Bad max_level parameter value: " ^ max_level_str) in
object
  method log_format = fmt
  method log_component = component
  method log_subchannel = subchannel
  method log_max_level = max_level
end


let stderr_logger_factory =
object
  method name = "stderr"
  method create_logger cf addr _ = 
    cf # restrict_subsections addr [];
    cf # restrict_parameters addr [ "type"; "format"; "component";
				    "subchannel"; "max_level"
				  ];
    let gc = new extract_generic_config cf addr in
    channel_logger_from_obj gc stderr
end


class file_logger (gc:generic_config) file : logger =
  let filter = mk_filter gc in
object(self)

  val mutable out = 
    open_out_gen [ Open_wronly; Open_append; Open_creat ] 0o666 file

  initializer
    Netsys_posix.register_post_fork_handler
      (object
	 method name = "Netplex_log.file_logger"
	 method run() = self # post_fork()
       end
      )


  method log_subch ~component ~subchannel ~level ~message =
    if filter component subchannel level then (
      let fmt = gc#log_format in
      try
	fprintf out "%s\n%!"
	  (format_message fmt component subchannel level message)
      with
	| error ->
	    prerr_endline
	      ("Netplex Catastrophic Error: Unable to write to log file " ^ 
		 file ^ ": " ^ Netexn.to_string error)
    )


  method log =
    self # log_subch ~subchannel:""

  method reopen() =
    close_out out;
    try
      out <-
	open_out_gen [ Open_wronly; Open_append; Open_creat ] 0o666 file
    with
      | error ->
	  prerr_endline
	    ("Netplex Catastrophic Error: Unable to reopen log file " ^ 
	       file ^ ": " ^ Netexn.to_string error)

  method private post_fork() =
    close_out out
end

let file_logger = new file_logger (new default_generic_config)

let file_logger_from_obj = new file_logger


let file_logger_factory =
object
  method name = "file"
  method create_logger cf addr _ =
    cf # restrict_subsections addr [];
    cf # restrict_parameters addr [ "type"; "file"; "format"; "component";
				    "subchannel"; "max_level"
				  ];
    let fileaddr =
      try
	cf # resolve_parameter addr "file"
      with
	| Not_found ->
	    failwith ("File logger needs parameter 'file'") in
    let file = cf # string_param fileaddr in
    let gc = new extract_generic_config cf addr in
    file_logger_from_obj gc file
end


class type multi_file_config =
object
  inherit generic_config
  method log_directory : string
  method log_files :
    (string * string * [ level | `All ] * string * string) list
end


let no_duplicates l =
  let h = Hashtbl.create 10 in
  List.filter
    (fun p ->
       not(Hashtbl.mem h p) && (
         Hashtbl.add h p ();
         true))
    l
;;


class multi_file_logger (mfc : multi_file_config) : logger =
  let log_files =
    List.map
      (fun (comp_pat, subch_pat, level, file, fmt) ->
	 let comp_re = regexp_of_pattern comp_pat in
	 let subch_re = regexp_of_pattern subch_pat in
	 (comp_re, subch_re, level, file, fmt)
      )
      mfc#log_files in
  let filter = mk_filter (mfc :> generic_config) in
object(self)
  val channels = Hashtbl.create 10
    (* Maps files to channels *)

  initializer
    Netsys_posix.register_post_fork_handler
      (object
	 method name = "Netplex_log.multi_file_logger"
	 method run() = self # post_fork()
       end
      )

  method log_subch ~component ~subchannel ~level ~message =
    if filter component subchannel level then (
      let w = level_weight level in
      let files =
	List.map
	  (fun (_, _, _, file, fmt) -> (file, fmt))
	  (List.filter
	     (fun (comp_re, subch_re, level_pat, _, _) ->
		match Netstring_str.string_match comp_re component 0 with
		  | Some _ ->
		      ( match 
			  Netstring_str.string_match subch_re subchannel 0
			with
			  | Some _ ->
			      ( match level_pat with
				  | `All -> true
				  | #level as l ->
				      w <= level_weight l
			      )
			  | None -> false
		      )
		  | None -> false
	     )
	     log_files
	  ) in
      let files = no_duplicates files in
      List.iter
	(fun (file, fmt) ->
	   let full_path =
	     if file <> "/" && file.[0] = '/' then
	       file
	     else
	       Filename.concat mfc#log_directory file
	   in
	   try
	     let ch = 
	       try
		 Hashtbl.find channels full_path
	       with
		 | Not_found ->
		     let ch =
	               open_out_gen 
			 [ Open_wronly; Open_append; Open_creat ] 0o666 full_path in
		     Hashtbl.add channels full_path ch;
		     ch
	     in
	     fprintf ch "%s\n%!"
	       (format_message fmt component subchannel level message)
	   with
	     | error ->
		 prerr_endline ("Netplex Catastrophic Error: Unable to write to log file " ^ full_path ^ ": " ^ Netexn.to_string error)

	)
	files
    )

  method log =
    self # log_subch ~subchannel:""

  method reopen() =
    Hashtbl.iter
      (fun name ch ->
	 close_out ch)
      channels;
    Hashtbl.clear channels

  method private post_fork() =
    self # reopen()

end


let multi_file_logger = new multi_file_logger

let multi_file_logger_factory =
object
  method name = "multi_file"
  method create_logger cf addr _ =
    cf # restrict_subsections addr [ "file" ];
    cf # restrict_parameters addr [ "type"; "format"; "component";
				    "subchannel"; "max_level";
				    "directory" ];
    let diraddr =
      try
	cf # resolve_parameter addr "directory"
      with
	| Not_found ->
	    failwith ("Multi-file logger needs parameter 'directory'") in
    let dir = cf # string_param diraddr in
    let gc = new extract_generic_config cf addr in
    let log_files =
      List.map
	(fun addr ->
	   cf # restrict_subsections addr [];
	   cf # restrict_parameters addr
	     [ "component"; "subchannel"; "max_level"; "file"; "format" ];
	   let component =
	     try 
	       cf # string_param (cf # resolve_parameter addr "component")
	     with
	       | Not_found -> "*" in
	   let subchannel =
	     try 
	       cf # string_param (cf # resolve_parameter addr "subchannel")
	     with
	       | Not_found -> "*" in
	   let max_level_str =
	     try
	       cf # string_param (cf # resolve_parameter addr "max_level")
	     with
	       | Not_found -> "all" in
	   let max_level =
	     try
               if STRING_LOWERCASE (max_level_str) = "all" then
		 `All
	       else
		 (level_of_string max_level_str :> [level | `All] )
	     with
	       | _ ->
		   failwith ("In section " ^ cf # print addr ^ 
			       ": Bad max_level parameter value: " ^ 
			       max_level_str) in
	   let fmt =
	     try cf # string_param (cf # resolve_parameter addr "format")
	     with Not_found -> gc#log_format in
	   let file =
	     try
	       cf # string_param (cf # resolve_parameter addr "file")
	     with
	       | Not_found ->
		   failwith ("In section " ^ cf # print addr ^ ": Parameter 'file' is missing") in
	   (component, subchannel, max_level, file, fmt)
	)
	(cf # resolve_section addr "file") in

    let config =
      ( object
	  method log_format = gc#log_format
	  method log_component = gc#log_component
	  method log_subchannel = gc#log_subchannel
	  method log_max_level = gc#log_max_level
	  method log_directory = dir
	  method log_files = log_files
	end
      ) in

    multi_file_logger config
end


class type syslog_config =
object
  inherit generic_config
  method log_identifier : string
  method log_facility : Netsys_posix.syslog_facility
end


let syslog_logger (sc:syslog_config) =
  let filter = mk_filter (sc :> generic_config) in
  let prepend = 
    if sc#log_identifier = "" then "" else sc#log_identifier ^ ": " in
object(self)
  method log_subch ~component ~subchannel ~level ~message =
    if filter component subchannel level then (
      try
	Netsys_posix.syslog
	  sc#log_facility
	  level
	  (prepend ^ message)
      with
	| error ->
	    prerr_endline
	      ("Netplex Catastrophic Error: Unable to write to syslog")
    )
  method log =
    self # log_subch ~subchannel:""

  method reopen() =
    ()
end


let facilities =
  [ "authpriv", `Authpriv;
    "cron", `Cron;
    "daemon", `Daemon;
    "ftp", `Ftp;
    "kern", `Kern;
    "local0", `Local0;
    "local1", `Local1;
    "local2", `Local2;
    "local3", `Local3;
    "local4", `Local4;
    "local5", `Local5;
    "local6", `Local6;
    "local7", `Local7;
    "lpr", `Lpr;
    "mail", `Mail;
    "news", `News;
    "syslog", `Syslog;
    "user", `User;
    "uucp", `Uucp;
    "default", `Default;
  ]



let syslog_logger_factory =
object
  method name = "syslog"
  method create_logger cf addr _ =
    cf # restrict_subsections addr [];
    cf # restrict_parameters addr [ "type"; "format"; "component";
				    "subchannel"; "max_level";
				    "identifier"; "facility"
				  ];
    let identifier =
      try cf # string_param (cf # resolve_parameter addr "identifier")
      with Not_found -> "" in
    let facility_str =
      try cf # string_param (cf # resolve_parameter addr "facility")
      with Not_found -> "default" in
    let facility =
      try List.assoc (STRING_LOWERCASE facility_str) facilities
      with Not_found ->
	failwith "Bad 'facility' parameter in syslog config" in
    let sc =
      ( object
	  inherit extract_generic_config cf addr 
	  method log_identifier = identifier
	  method log_facility = facility
	end
      ) in
    syslog_logger sc
end



let logger_factories =
  [ file_logger_factory;
    multi_file_logger_factory;
    stderr_logger_factory;
    syslog_logger_factory
  ]
