(* Required for compat with OCaml < 4.04 *)
let extension filename =
  let length = String.length filename in
  let last_dot =
    try String.rindex filename '.' with Not_found -> length - 1
  in
  String.sub filename last_dot (length - last_dot)

let read_file filename =
  let lines = ref [] in
  let chan = open_in filename in
  try
    while true do
      lines := input_line chan :: !lines
    done;
    assert false
  with End_of_file ->
    close_in chan;
    List.rev !lines

let pp_string_list = Fmt.(list ~sep:(const string " ") string)

let pp_options fmt = function
  | [] -> ()
  | options ->
      pp_string_list fmt options;
      Fmt.pf fmt " "

let read_dir dir = Sys.readdir dir |> Array.to_list |> List.sort String.compare

let get_dirs dir =
  let is_dir f = Filename.concat dir f |> Sys.is_directory in
  read_dir dir |> List.filter is_dir

let get_files dir =
  let is_file f = not (Filename.concat dir f |> Sys.is_directory) in
  read_dir dir |> List.filter is_file

let cwd_options_file = "test-case.opts"

let cwd_test_files =
  [ "test-case.md"; "test-case.t"; "test-case.mli"; "test-case.mld" ]

let cwd_enabled_if_file = "test-case.enabled-if"

type dir = {
  test_file : string;
  target_file : string;
  expected_file : string;
  options : string list;
  dir_name : string;
  enabled_if : string list option;
}

let test_file ~dir_name files =
  let is_test_file f = List.mem f cwd_test_files in
  match List.filter is_test_file files with
  | [ test_file ] -> test_file
  | found_files ->
      Format.eprintf "Invalid number of test file for %s (found %a)\n" dir_name
        pp_string_list found_files;
      Format.eprintf "There should be exactly one of [%a]\n" pp_string_list
        cwd_test_files;
      exit 1

let expected_file ~dir_name ~test_file files =
  let is_expected_file f = extension f = ".expected" in
  match List.filter is_expected_file files with
  | [] -> test_file
  | [ expected_file ] -> expected_file
  | _ ->
      Printf.eprintf "More than one .expected file for %s\n" dir_name;
      exit 1

let dir dir_name =
  let files = get_files dir_name in
  let test_file = test_file ~dir_name files in
  let expected_file = expected_file ~dir_name ~test_file files in
  let target_file = dir_name ^ ".actual" in
  let options_file = Filename.concat dir_name cwd_options_file in
  let options_file_exists = Sys.file_exists options_file in
  let options = if options_file_exists then read_file options_file else [] in
  let enabled_if_file = Filename.concat dir_name cwd_enabled_if_file in
  let enabled_if_file_exists = Sys.file_exists enabled_if_file in
  let enabled_if =
    if enabled_if_file_exists then Some (read_file enabled_if_file) else None
  in
  { test_file; target_file; expected_file; options; dir_name; enabled_if }

let enabled_if = function
  | None -> ""
  | Some lines -> Format.asprintf "\n (enabled_if %a)" pp_string_list lines

let pr_runtest_alias dir =
  Fmt.pr {|
(rule
 (alias runtest)%s
 (action (diff %s/%s %s)))
|}
    (enabled_if dir.enabled_if)
    dir.dir_name dir.expected_file dir.target_file

let pr_rule ~pp_action dir =
  Fmt.pr
    {|
(rule
 (target %s)
 (deps (package mdx) (source_tree %s))
 (action%a))
|}
    dir.target_file dir.dir_name pp_action dir

type generator = {
  pp_expect_action : Format.formatter -> dir -> unit;
  pp_failure_action : Format.formatter -> dir -> unit;
}

let pr_rule ~pp_action dir_name =
  let dir = dir dir_name in
  pr_rule ~pp_action dir;
  pr_runtest_alias dir

let rule_gen generator rule_type () =
  let pp_action =
    match rule_type with
    | `Test_expect -> generator.pp_expect_action
    | `Test_failure -> generator.pp_failure_action
  in
  let dirs =
    List.filter
      (function
        | ".formatted" -> (* This is generated by @fmt *) false | _ -> true)
      (get_dirs ".")
  in
  List.iter (pr_rule ~pp_action) dirs

let run generator =
  let open Cmdliner in
  let cmds =
    Term.
      [
        (let term = const (rule_gen generator `Test_expect) $ const () in
         let info = Cmd.info "test_expect" in
         Cmd.v info term);
        (let term = const (rule_gen generator `Test_failure) $ const () in
         let info = Cmd.info "test_failure" in
         Cmd.v info term);
      ]
  in
  let info =
    let doc = "Generate dune files for the binary tests." in
    let man = [] in
    Cmd.info "gen_dune_rules" ~doc ~man
  in
  let default = Term.(ret (const (`Help (`Auto, None)))) in
  let group = Cmd.group ~default info cmds in
  Stdlib.exit @@ Cmd.eval group
