(**************************************************************************)
(*  Copyright 2003, 2002 b8_bavard, b8_zoggy, , b52_simon INRIA            *)
(*                                                                        *)
(*    This file is part of mldonkey.                                      *)
(*                                                                        *)
(*    mldonkey is free software; you can redistribute it and/or modify    *)
(*    it under the terms of the GNU General Public License as published   *)
(*    by the Free Software Foundation; either version 2 of the License,   *)
(*    or (at your option) any later version.                              *)
(*                                                                        *)
(*    mldonkey is distributed in the hope that it will be useful,         *)
(*    but WITHOUT ANY WARRANTY; without even the implied warranty of      *)
(*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the       *)
(*    GNU General Public License for more details.                        *)
(*                                                                        *)
(*    You should have received a copy of the GNU General Public License   *)
(*    along with mldonkey; if not, write to the Free Software             *)
(*    Foundation, Inc., 59 Temple Place, Suite 330, Boston,               *)
(*    MA  02111-1307  USA                                                 *)
(*                                                                        *)
(**************************************************************************)


open Printf2
  
(** Simple options:
  This will enable very simple configuration, by a mouse-based configurator.
  Options will be defined by a special function, which will also check
  if a value has been provided  by the user in its .gwmlrc file.
  The .gwmlrc will be created by a dedicated tool, which could be used
  to generate both .gwmlrc and .efunsrc files.

Note: this is redundant, since such options could also be better set
in the .Xdefaults file (using Xrm to load them). Maybe we should merge
both approaches in a latter release.
  
   Code from Fabrice Le Fessant.

    *)
  
type option_value =
    Module of option_module
  | StringValue of string
  | IntValue of int
  | FloatValue of float
  | List of option_value list
  | SmallList of option_value list
and option_module = (string * option_value) list
;;



type 'a option_class =
  { class_name : string;
    from_value : option_value -> 'a;
    to_value : 'a -> option_value;
    mutable class_hooks : ('a option_record -> unit) list }
  
and 'a option_record =
  { option_name : string list;
    option_class : 'a option_class;
    mutable option_value : 'a;
    option_help : string;
    mutable option_hooks : (unit -> unit) list;
    mutable string_wrappers : (('a -> string) * (string -> 'a)) option;
    option_file : options_file;
  }
  
and options_file = {
    mutable file_name : string; 
    mutable file_options : Obj.t option_record list;
    mutable file_rc : option_module;
    mutable file_pruned : bool;
  }
;;

let create_options_file name =
  ignore
    (
     if not (Sys.file_exists name) then
       let oc = open_out name in 
       close_out oc
    );
  {
    file_name = name;
    file_options =[];
    file_rc = [];
    file_pruned = false;
  }
  
let set_options_file opfile name = opfile.file_name <- name

let
  define_option_class
    (class_name : string)
    (from_value : option_value -> 'a)
    (to_value : 'a -> option_value) =
  let c =
    {class_name = class_name; 
      from_value = from_value; 
      to_value = to_value;
     class_hooks = []}
  in
  c
;;  

(*
let filename =
  ref
    (Filename.concat Sysenv.home
       ("." ^ Filename.basename Sys.argv.(0) ^ "rc"))
;;
let gwmlrc = ref [];;

let options = ref [];;
*)

let rec find_value list m =
  match list with
    [] -> raise Not_found
  | name :: tail ->
      let m = List.assoc name m in
      match m, tail with
        _, [] -> m
      | Module m, _ :: _ -> find_value tail m
      | _ -> raise Not_found
;;

let prune_file file =
  file.file_pruned <- true

let
  define_option
    (opfile : options_file)
    (option_name : string list)
    (option_help : string)
    (option_class : 'a option_class)
    (default_value : 'a) =
  let o =
    {option_name = option_name; 
      option_help = option_help;
      option_class = option_class; 
      option_value = default_value;
      string_wrappers = None;
      option_hooks = []; 
      option_file = opfile; }
  in
  opfile.file_options <- (Obj.magic o : Obj.t option_record) ::
    opfile.file_options;
  o.option_value <-
    begin try o.option_class.from_value (find_value option_name 
        opfile.file_rc) with
      Not_found -> default_value
    | e ->
        lprintf "Options.define_option, for option %s: "
          (match option_name with
             [] -> "???"
           | name :: _ -> name);
        lprintf_nl "%s" (Printexc2.to_string e);
        default_value
    end;
  o
;;

  
open Genlex;;
  
let lexer = make_lexer ["="; "{"; "}"; "["; "]"; ";"; "("; ")"; ","; "."];;
  
let rec parse_gwmlrc (strm__ : _ Stream.t) =
  match
    try Some (parse_id strm__) with
      Stream.Failure -> None
  with
    Some id ->
      begin match Stream.peek strm__ with
        Some (Kwd "=") ->
          Stream.junk strm__;
          let v =
            try parse_option strm__ with
              Stream.Failure -> raise (Stream.Error "")
          in
          let eof =
            try parse_gwmlrc strm__ with
              Stream.Failure -> raise (Stream.Error "")
          in
          (id, v) :: eof
      | _ -> raise (Stream.Error "")
      end
  | _ -> []
and parse_option (strm__ : _ Stream.t) =
  match Stream.peek strm__ with
    Some (Kwd "{") ->
      Stream.junk strm__;
      let v =
        try parse_gwmlrc strm__ with
          Stream.Failure -> raise (Stream.Error "")
      in
      begin match Stream.peek strm__ with
        Some (Kwd "}") -> Stream.junk strm__; Module v
      | _ -> raise (Stream.Error "")
      end
  | Some (Ident s) -> Stream.junk strm__; StringValue s
  | Some (String s) -> Stream.junk strm__; StringValue s
  | Some (Int i) -> Stream.junk strm__; IntValue i
  | Some (Float f) -> Stream.junk strm__; FloatValue f
  | Some (Char c) ->
      Stream.junk strm__;
      StringValue (let s = String.create 1 in s.[0] <- c; s)
  | Some (Kwd "[") ->
      Stream.junk strm__;
      let v =
        try parse_list strm__ with
          Stream.Failure -> raise (Stream.Error "")
      in
      List v
  | Some (Kwd "(") ->
      Stream.junk strm__;
      let v =
        try parse_list strm__ with
          Stream.Failure -> raise (Stream.Error "")
      in
      List v
  | _ -> raise Stream.Failure
and parse_id (strm__ : _ Stream.t) =
  match Stream.peek strm__ with
    Some (Ident s) -> Stream.junk strm__; s
  | Some (String s) -> Stream.junk strm__; s
  | _ -> raise Stream.Failure
and parse_list (strm__ : _ Stream.t) =
  match Stream.peek strm__ with
    Some (Kwd ";") ->
      Stream.junk strm__;
      begin try parse_list strm__ with
        Stream.Failure -> raise (Stream.Error "")
      end
  | Some (Kwd ",") ->
      Stream.junk strm__;
      begin try parse_list strm__ with
        Stream.Failure -> raise (Stream.Error "")
      end
  | Some (Kwd ".") ->
      Stream.junk strm__;
      begin try parse_list strm__ with
        Stream.Failure -> raise (Stream.Error "")
      end
  | _ ->
      match
        try Some (parse_option strm__) with
          Stream.Failure -> None
      with
        Some v ->
          let t =
            try parse_list strm__ with
              Stream.Failure -> raise (Stream.Error "")
          in
          v :: t
      | _ ->
          match Stream.peek strm__ with
            Some (Kwd "]") -> Stream.junk strm__; []
          | Some (Kwd ")") -> Stream.junk strm__; []
          | _ -> raise Stream.Failure
;;

let exec_hooks o =
  List.iter
    (fun f ->
       try f () with
         _ -> ())
    o.option_hooks
;;  

let exec_chooks o =
  List.iter
    (fun f ->
       try f o with
         _ -> ())
    o.option_class.class_hooks
;;  
  
let really_load filename options =
  let temp_file = filename ^ ".tmp" in
  if Sys.file_exists temp_file then begin
    let buf = Buffer.create 150 in
    Printf.bprintf buf "File %s exists\n" temp_file;
    Printf.bprintf buf
      "An error may have occurred during previous configuration save.\n";
    Printf.bprintf buf
      "Please, check your configurations files, and rename/remove this file\n";
    Printf.bprintf buf "before restarting.";
    raise (Failure (Buffer.contents buf))
  end 
  else
    let ic = open_in filename in
    let s = Stream.of_channel ic in
    try
      let stream = lexer s in
      let list =
	try parse_gwmlrc stream with
          e ->
            lprintf_nl "At pos %d/%d" (Stream.count s) (Stream.count stream);
            raise e
      in
      List.iter
	(fun o ->
          try
            o.option_value <-
              o.option_class.from_value (find_value o.option_name list);
            exec_chooks o;
            exec_hooks o
          with
            e ->
             lprintf_nl "Exc %s" (Printexc2.to_string e))
	options;
      list
    with
      e ->
	lprintf_nl "Error %s in %s" (Printexc2.to_string e) filename;
	[]
;;
      
let load opfile =
  try opfile.file_rc <- really_load opfile.file_name opfile.file_options with
    Not_found -> 
      lprintf_nl "No %s found" opfile.file_name
;;

let append opfile filename =
  try opfile.file_rc <-
    really_load filename opfile.file_options @ opfile.file_rc with
    Not_found -> 
      lprintf_nl "No %s found" filename
;;
      
let ( !! ) o = o.option_value;;
let ( =:= ) o v = o.option_value <- v; exec_chooks o; exec_hooks o;;
    
let value_to_string v =
  match v with
    StringValue s -> s
  | IntValue i -> string_of_int i
  | FloatValue f -> string_of_float f
  | _ -> failwith "Options: not a string option"
;;
      
let string_to_value s = StringValue s;;
  
let value_to_int v =
  match v with
    StringValue s -> int_of_string s
  | IntValue i -> i
  | _ -> failwith "Options: not an int option"
;;

let int_to_value i = IntValue i;;

(* The Pervasives version is too restrictive *)
let bool_of_string s =
  match String.lowercase s with
    "true" -> true
  | "false" -> false
  | "yes" -> true
  | "no" -> false
  | "y" -> true
  | "n" -> false
  | _ -> invalid_arg "bool_of_string"
;;

let value_to_bool v =
  match v with
    StringValue s -> bool_of_string s
  | IntValue v when v = 0 -> false
  | IntValue v when v = 1 -> true
  | _ -> failwith "Options: not a bool option"
;;
let bool_to_value i = StringValue (string_of_bool i);;

let value_to_float v =
  match v with
    StringValue s -> float_of_string s
  | FloatValue f -> f
  | _ -> failwith "Options: not a float option"
;; 

let float_to_value i = FloatValue i;;

let value_to_string2 v =
  match v with
    List [s1; s2] | SmallList [s1;s2] -> 
      value_to_string s1, value_to_string s2
  | _ -> failwith "Options: not a string2 option"
;;

let string2_to_value (s1, s2) = SmallList [StringValue s1; StringValue s2];;

let value_to_list v2c v =
  match v with
    List l | SmallList l -> List.rev (List.rev_map v2c l)
  | StringValue s -> failwith (Printf.sprintf 
        "Options: not a list option (StringValue [%s])" s)
  | FloatValue _ -> failwith "Options: not a list option (FloatValue)"
  | IntValue _ -> failwith "Options: not a list option (IntValue)"
  | Module _ -> failwith "Options: not a list option (Module)"
;;

let list_to_value c2v l =
  List
    (List.fold_right
      (fun v list ->
          try c2v v :: list with
            _ -> list)
       l [])
;;
  
let smalllist_to_value c2v l =
  SmallList
    (List.fold_right
       (fun v list ->
          try c2v v :: list with
            _ -> list)
       l [])
;;

let string_option =
  define_option_class "String" value_to_string string_to_value
;;
let color_option =
  define_option_class "Color" value_to_string string_to_value
;;
let font_option = define_option_class "Font" value_to_string string_to_value;;

let int_option = define_option_class "Int" value_to_int int_to_value;;

let bool_option = define_option_class "Bool" value_to_bool bool_to_value;;
let float_option = define_option_class "Float" value_to_float float_to_value;;

let string2_option =
  define_option_class "String2" value_to_string2 string2_to_value
;;

let list_option cl =
  define_option_class (cl.class_name ^ " List") (value_to_list cl.from_value)
    (list_to_value cl.to_value)
;;

let smalllist_option cl =
  define_option_class (cl.class_name ^ " List") (value_to_list cl.from_value)
    (smalllist_to_value cl.to_value)
;;

let to_value cl = cl.to_value;;
let from_value cl = cl.from_value;;
  
let value_to_sum l v =
  match v with
    StringValue s -> List.assoc s l
  | _ -> failwith "Options: not a sum option"
;;
  
let sum_to_value l v = StringValue (List.assq v l);;
  
let sum_option l =
  let ll = List.map (fun (a1, a2) -> a2, a1) l in
  define_option_class "Sum" (value_to_sum l) (sum_to_value ll)
;;

let exit_exn = Exit;;
let safe_string s =
  if s = "" then "\"\""
  else
    try
      match s.[0] with
        'a'..'z' | 'A'..'Z' ->
          for i = 1 to String.length s - 1 do
            match s.[i] with
              'a'..'z' | 'A'..'Z' | '_' | '0'..'9' -> ()
            | _ -> raise exit_exn
          done;
          s
      | _ ->
          if string_of_int (int_of_string s) = s ||
             string_of_float (float_of_string s) = s then
            s
          else raise exit_exn
    with
      _ -> Printf.sprintf "\"%s\"" (String.escaped s)
;;

let with_help = ref false;;
      
let rec save_module indent oc list =
  let subm = ref [] in
  List.iter
    (fun (name, help, value) ->
       match name with
         [] -> assert false
       | [name] ->
           if !with_help && help <> "" then
             Printf.fprintf oc "(* %s *)\n" help;
           Printf.fprintf oc "%s %s = " indent (safe_string name);
           save_value indent oc value;
           Printf.fprintf oc "\n"
       | m :: tail ->
           let p =
             try List.assoc m !subm with
               _ -> let p = ref [] in subm := (m, p) :: !subm; p
           in
           p := (tail, help, value) :: !p)
    list;
  List.iter
    (fun (m, p) ->
       Printf.fprintf oc "%s %s = {\n" indent (safe_string m);
       save_module (indent ^ "  ") oc !p;
       Printf.fprintf oc "%s}\n" indent)
    !subm
and save_list indent oc list =
  match list with
    [] -> ()
  | [v] -> save_value indent oc v
  | v :: tail ->
      save_value indent oc v; Printf.fprintf oc ", "; save_list indent oc tail
and save_list_nl indent oc list =
  match list with
    [] -> ()
  | [v] -> Printf.fprintf oc "\n%s" indent; save_value indent oc v
  | v :: tail ->
      Printf.fprintf oc "\n%s" indent;
      save_value indent oc v;
      Printf.fprintf oc ";";
      save_list_nl indent oc tail
and save_value indent oc v =
  match v with
    StringValue s -> Printf.fprintf oc "%s" (safe_string s)
  | IntValue i -> Printf.fprintf oc "%d" i
  | FloatValue f -> Printf.fprintf oc "%f" f
  | List l ->
      Printf.fprintf oc "[";
      save_list_nl (indent ^ "  ") oc l;
      Printf.fprintf oc "]"
  | SmallList l ->
      Printf.fprintf oc "(";
      save_list (indent ^ "  ") oc l;
      Printf.fprintf oc ")"
  | Module m -> 
      Printf.fprintf oc "{";
      save_module_fields (indent ^ "  ") oc m;
      Printf.fprintf oc "}"
      
and save_module_fields indent oc m =
  match m with
    [] -> ()
  | (name, v) :: tail ->
      Printf.fprintf oc "%s %s = " indent (safe_string name);
      save_value indent oc v;
      Printf.fprintf oc "\n";
      save_module_fields indent oc tail
;;
    
let save opfile =
  let filename = opfile.file_name in
  let temp_file = filename ^ ".tmp" in
  let old_file = filename ^ ".old" in
  let oc = open_out temp_file in
  save_module "" oc
    (List.map
      (fun o ->
        o.option_name, o.option_help,
        (try 
            o.option_class.to_value o.option_value 
          with
            e ->
              lprintf_nl "Error while saving option \"%s\": %s"
                (try List.hd o.option_name with
                  _ -> "???")
              (Printexc2.to_string e);
              StringValue ""))
    (List.rev opfile.file_options));
  if not opfile.file_pruned then begin
      Printf.fprintf oc
        "\n(*\n The following options are not used (errors, obsolete, ...) \n*)\n";
      List.iter
        (fun (name, value) ->
          try
            List.iter
              (fun o ->
                match o.option_name with
                  n :: _ -> if n = name then raise Exit
                | _ -> ())
            opfile.file_options;
            Printf.fprintf oc "%s = " (safe_string name);
            save_value "  " oc value;
            Printf.fprintf oc "\n"
          with
            _ -> ())
      opfile.file_rc;
    end;
  close_out oc;
  (try Sys.rename filename old_file with _ -> ());
  (try Sys.rename temp_file filename with _ -> ())
;;

let save_with_help opfile =
  with_help := true;
  begin try save opfile with
    _ -> ()
  end;
  with_help := false
;;
  
let option_hook option f = option.option_hooks <- f :: option.option_hooks;;
  
let class_hook option_class f =
  option_class.class_hooks <- f :: option_class.class_hooks
;;

let rec iter_order f list =
  match list with
    [] -> ()
  | v :: tail -> f v; iter_order f tail
;;
  
let help oc opfile =
  List.iter
    (fun o ->
       Printf.fprintf oc "OPTION \"";
       begin match o.option_name with
         [] -> Printf.fprintf oc "???"
       | [name] -> Printf.fprintf oc "%s" name
       | name :: tail ->
           Printf.fprintf oc "%s" name;
           iter_order (fun name -> Printf.fprintf oc ":%s" name) o.option_name
       end;
       Printf.fprintf oc "\" (TYPE \"%s\"): %s\n   CURRENT: \n"
         o.option_class.class_name o.option_help;
       begin try
         save_value "" oc (o.option_class.to_value o.option_value)
       with
         _ -> ()
       end;
       Printf.fprintf oc "\n")
    opfile.file_options;
  flush oc
;;
  
    
let tuple2_to_value (c1, c2) (a1, a2) =
  SmallList [to_value c1 a1; to_value c2 a2]
;;
  
let value_to_tuple2 (c1, c2) v =
  match v with
    List [v1; v2] -> from_value c1 v1, from_value c2 v2
  | SmallList [v1; v2] -> from_value c1 v1, from_value c2 v2
  | List l | SmallList l ->
      lprintf_nl "list of %d" (List.length l);
      failwith "Options: not a tuple2 list option"
  | _ -> failwith "Options: not a tuple2 option"
;;
  
let tuple2_option p =
  define_option_class "tuple2_option" (value_to_tuple2 p) (tuple2_to_value p)
;;
  
let tuple3_to_value (c1, c2, c3) (a1, a2, a3) =
  SmallList [to_value c1 a1; to_value c2 a2; to_value c3 a3]
;;
let value_to_tuple3 (c1, c2, c3) v =
  match v with
    List [v1; v2; v3] -> from_value c1 v1, from_value c2 v2, from_value c3 v3
  | SmallList [v1; v2; v3] ->
      from_value c1 v1, from_value c2 v2, from_value c3 v3
  | _ -> failwith "Options: not a tuple3 option"
;;
      
let tuple3_option p =
  define_option_class "tuple3_option" (value_to_tuple3 p) (tuple3_to_value p)
;;

let shortname o = String.concat ":" o.option_name;;
let get_class o = o.option_class;;
let get_help o =
  let help = o.option_help in if help = "" then "No Help Available" else help
;;


let simple_options opfile =
  let list = ref [] in
  List.iter (fun o ->
      match o.option_name with
        [] | _ :: _ :: _ -> ()
      | [name] ->
          match o.option_class.to_value o.option_value with
            Module _ | SmallList _ | List _ -> 
              begin
                match o.string_wrappers with
                  None -> ()
                | Some (to_string, from_string) ->
                    list := (name, to_string o.option_value) :: !list   
              end
          | v -> 
              list := (name, value_to_string v) :: !list
  ) opfile.file_options;
  !list

let get_option opfile name =
  let rec iter name list = 
    match list with 
      [] -> raise Not_found
    | o :: list ->
        if o.option_name = name then o
        else iter name list
  in
  iter [name] opfile.file_options
  
  
let set_simple_option opfile name v =
  let o = get_option opfile name in
  begin
    match o.string_wrappers with
      None ->
        o.option_value <- o.option_class.from_value (string_to_value v);
    | Some (_, from_string) -> 
        o.option_value <- from_string v
  end;
  exec_chooks o; exec_hooks o;;
    
let get_simple_option opfile name =
  let o = get_option opfile name in
  match o.string_wrappers with
    None ->
      value_to_string (o.option_class.to_value o.option_value)
  | Some (to_string, _) -> 
      to_string o.option_value
  
let set_option_hook opfile name hook =
  let o = get_option opfile name in
  o.option_hooks <- hook :: o.option_hooks
  
let set_string_wrappers o to_string from_string =
  o.string_wrappers <- Some (to_string, from_string)

let simple_args opfile =
  List.map (fun (name, v) ->
      ("-" ^ name), 
      Arg.String (set_simple_option opfile name), 
      (Printf.sprintf "<string> : \t%s (current: %s)"
          (get_option opfile name).option_help
          v)
  ) (simple_options opfile)
