open Ext.Fugue
open Ext.Filepath
open Ext
open Helper
open Printf
open Gconf

exception ConfigChanged of string
exception ToolNotFound of filename
exception ConfigurationMissingKey of string
exception ConfigurationTypeMismatch of string * string * string
exception ConfigureScriptFailed of string

type flag_action = SetFlag of string | ClearFlag of string

let getDigestKV () =
    let digest = Project.digest () in
    [ ("obuild-digest", digest) ]

let generateMlFile project file flags =
    Utils.generateFile file (fun add ->
        add "(* autogenerated file by obuild. do not modify *)\n";
        add (sprintf "let project_version = \"%s\"\n" project.Analyze.project_file.Project.version);
        (* TODO escape name properly *)
        List.iter (fun (name, v) -> add (sprintf "let project_flag_%s = %b\n" name v)) flags;
    )

let generateCFile project file flags =
    Utils.generateFile file (fun add ->
        add "/* autogenerated file by obuild. do not modify */\n";
        add (sprintf "#define PROJECT_VERSION \"%s\"\n" project.Analyze.project_file.Project.version);
        (* TODO escape name properly *)
        List.iter (fun (name, v) ->
            add (sprintf "#define PROJECT_FLAG_%s %d\n" (String.uppercase name) (if v then 1 else 0))
        ) flags;
    )

let makeSetup digestKV project flags = hashtbl_fromList (
    digestKV
    @ hashtbl_toList project.Analyze.project_ocamlcfg
    @ List.map (fun (opt,v) -> (opt, string_of_bool v)) (Gconf.get_target_options ())
    @ List.map (fun (flagname,flagval) -> ("flag-" ^ flagname, string_of_bool flagval)) flags
  )

let sanityCheck () =
    let (_: string) = Prog.getOcamlOpt () in
    let (_: string) = Prog.getOcamlC () in
    let (_: string) = Prog.getOcamlDep () in
    ()

let comparekvs reason setup l = List.iter (fun (k,v) ->
    try
      let v' = Hashtbl.find setup k in
      if v' <> v then
        raise (ConfigChanged reason)
    with Not_found ->
      raise (ConfigChanged reason)
  ) l

let comparekvs_hashtbl reason setup l = Hashtbl.iter (fun k v ->
    try
      let v' = Hashtbl.find setup k in
      if v' <> v then
        raise (ConfigChanged reason)
    with Not_found ->
      raise (ConfigChanged reason)
  ) l

let execute_configure_script proj_file =
  match proj_file.Project.configure_script with
  | None -> ()
  | Some script ->
    let args = [ (Prog.getOcaml ()); (fp_to_string script) ] in
    (match Process.run args with
    | Process.Success (_, warnings,_) -> print_warnings warnings
    | Process.Failure er -> raise (ConfigureScriptFailed er))

let create_dist project flags =
  verbose Verbose "configuration changed, deleting dist\n%!";
  Filesystem.removeDirContent (Dist.build_path);
  Dist.remove_dead_links ();
  verbose Verbose "auto-generating configuration files\n%!";
  let autogenDir = Dist.create_build Dist.Autogen in
  generateMlFile project (autogenDir </> fn "path_generated.ml") flags;
  generateCFile project (autogenDir </> fn "obuild_macros.h") flags

let get_assoc name assoc =
  try
    let v = List.assoc name assoc in
    Some v
  with Not_found -> None

let get_flags_value proj_file setup_flags user_flags = List.map (fun flag ->
    let name = flag.Project.Flag.name in
    let def  = flag.Project.Flag.default in
    let override = ref (get_assoc name setup_flags) in
    List.iter (fun tw -> match tw with
        | ClearFlag s -> if s = name then override := Some false
        | SetFlag   s -> if s = name then override := Some true
      ) user_flags;
    match (!override, def) with
    | (None, None)   -> (name, false)
    | (None, Some v) -> (name, v)
    | (Some v, _)    -> (name, v)
  ) proj_file.Project.flags

let check_extra_tools proj_file =
  let syspath = Utils.get_system_paths () in
  List.iter (fun tool ->
      try let _ = Utils.find_in_paths syspath tool in ()
      with Utils.FileNotFoundInPaths _ -> raise (ToolNotFound tool)
    ) proj_file.Project.extra_tools

let get_flags hash =
  Hashtbl.fold (fun k v acc ->
      if string_startswith "flag-" k then
        (string_drop 5 k, bool_of_string v) :: acc
      else
        acc) hash []

let bool_of_opt hashtable k =
  let get_opt k =
    try Hashtbl.find hashtable k
    with Not_found -> raise (ConfigurationMissingKey k)
  in
  let v = get_opt k in
  try bool_of_string v
  with Failure _ -> raise (ConfigurationTypeMismatch (k,"bool",v))

let set_opts hashtable = (* load the environment *)
  let opts = Gconf.get_target_options_keys () in
  List.iter (fun k -> Gconf.set_target_options k (bool_of_opt hashtable k)) opts

let check_ocaml () =
  let ocamlCfg = Prog.getOcamlConfig () in
  let ocaml_ver = Hashtbl.find ocamlCfg "version" in
  let ver = string_split '.' ocaml_ver in
  (match ver with
   | major::minor::_-> (
       if int_of_string major < 4 then gconf.bin_annot <- false;
       if int_of_string major > 4 && int_of_string minor > 1 then gconf.short_path <- true
     )
   | _ -> gconf.bin_annot <- false
  );
  ocamlCfg

let run proj_file user_flags user_opts =
  Dist.create_maybe ();
  let _ = check_ocaml () in
  let digestKV = getDigestKV () in
  execute_configure_script proj_file;
  let configure = try Some (Dist.read_configure ()) with _ -> None in
  let configure_flags = match configure with
    | None -> []
    | Some h ->
      (* set opts and return the flags *)
      Hashtbl.iter (fun k _ ->
          if not (string_startswith "flag-" k) then
            Gconf.set_target_options k (bool_of_opt h k)
        ) h;
      get_flags h in
  let flags = get_flags_value proj_file configure_flags user_flags in
  verbose Debug "  configure flag: [%s]\n" (Utils.showList "," (fun (n,v) -> n^"="^string_of_bool v) flags);
  check_extra_tools proj_file;
  let project = Analyze.prepare proj_file flags in
  (* let's set the user opts before saving the setup file *)
  List.iter (fun (o,v) -> Gconf.set_target_options o v) user_opts;
  let currentSetup = makeSetup digestKV project flags in
  let actualSetup = try Some (Dist.read_setup ()) with _ -> None in
  let projectSystemChanged = match actualSetup with
    | None     -> true
    | Some stp ->
      (* TODO harcoded for now till we do all the checks. *)
      try comparekvs "setup" stp (hashtbl_toList currentSetup); (* FORCED should be false *) true
      with _ -> true
  in

  if projectSystemChanged then (
    create_dist project flags;
    (* write setup file *)
    verbose Verbose "Writing new setup\n%!";
    Dist.write_setup currentSetup
  )

let check proj_file reconf setup =
  let ocamlCfg = check_ocaml () in
  let digestKV = getDigestKV () in
  (* check if the environment changed. *)
  comparekvs_hashtbl "ocaml config" setup ocamlCfg;
  (* if the digest of .obuild changed, let's reconfigure *)
  let reconfigure = try
      comparekvs "digest" setup digestKV;
      false
    with e -> if reconf then true else raise e in
  (* user_flags are also restored from setup file *)
  let setup_flags = get_flags setup in
  let flags = get_flags_value proj_file setup_flags [] in
  (* .obuild changed, maybe we should compare a little bit deeper to not retriggerd reconf too often ... *)
  if reconfigure then begin
    (* let's call configure-script if available, however we don't care about the content of dist/configure *)
    execute_configure_script proj_file;
    verbose Debug "  configure flag: [%s]\n" (Utils.showList "," (fun (n,v) -> n^"="^string_of_bool v) flags);
    check_extra_tools proj_file;
    let project = Analyze.prepare proj_file flags in
    create_dist project flags;
    (* write setup file *)
    verbose Verbose "Writing new setup\n%!";
    let current_setup = makeSetup digestKV project flags in
    Dist.write_setup current_setup
  end;
  flags
