open Core_kernel
open Cwe_checker_core.Main

exception InvalidPathException of string
exception NoOutputFileException of string
exception NoModulesException of string
exception NoConfigException of string
exception NoBinaryPathException of string


let rec get_difference (set_a : 'a list) (set_b: 'a list) : 'a list =
  match set_a with
  | [] -> []
  | element_of_a::remain_a ->
    match (Stdlib.List.mem element_of_a set_b) with
    | true  -> get_difference remain_a set_b
    | false -> List.append (get_difference remain_a set_b) [element_of_a]


let get_user_input ?(position=1) (() : unit) : string list =
  Array.to_list (Array.sub Sys.argv ~pos:position ~len:(Array.length Sys.argv - position))


let rec find_prefix (input : string list) (prefix : string) : string option =
  match input with
  | []         -> None
  | head::tail ->
    match (String.is_prefix head ~prefix:prefix) with
    | true  -> Some head
    | false -> find_prefix tail prefix


let rec replace_element (set : string list) (element : string) (replacement : string) : string list =
  match set with
  | []         -> []
  | head::tail ->
    match String.is_prefix ~prefix:element head with
    | true  -> replacement::tail
    | false -> head::replace_element tail element replacement


let rec remove_element (flags : string list) (element: string): string list =
  match flags with
  | [] -> []
  | head::tail ->
      match String.is_prefix ~prefix:element head with
      | true -> tail
      | false -> head::remove_element tail element


let check_config (input : string list) : unit =
  match find_prefix input "-config" with
  | None   -> Cwe_checker_core.Log_utils.info "Using standard configuration..."
  | Some c ->
    match Stdlib.List.nth_opt (String.split c ~on:'=') 1 with
    | None | Some ""-> raise (NoConfigException "No config file provided. If -config flag set please provide a config file.")
    | Some f  -> if (Sys.file_exists f) then () else raise (InvalidPathException "Path to config file not valid")


let check_output_path (input : string list) : unit =
  match find_prefix input "-out" with
  | Some param -> begin
      try
        match Stdlib.List.nth (String.split param ~on:'=') 1 with
        | ""   -> raise (NoOutputFileException "No output file provided. If -out flag is set please provide an out file.")
        | _  -> ()
      with
      | _ -> raise (NoOutputFileException "No output file provided. If -out flag is set please provide an out file.")
    end
  | None -> ()


let setup_flags (flags : string list) : string =
  String.concat ~sep:" " (List.map ~f:(fun pre -> "--cwe-checker" ^ pre) flags)


let check_partial (input : string list) : unit =
  match find_prefix input "-partial" with
  | None   -> ()
  | Some p ->
    match Stdlib.List.nth_opt (String.split p ~on:'=') 1 with
    | None | Some "" ->  raise (NoModulesException "No modules provided. If -partial flag is set please provide the corresponding modules.")
    | Some modules   ->  check_valid_module_list (String.split_on_chars modules ~on:[','])


let validate_user_input (input : string list) : unit =
  let valid_flags = List.map ~f:(fun x -> "-" ^ fst x) Cwe_checker_core.Main.cmdline_flags in

  match get_difference input valid_flags with
  | []       -> ()
  | invalid  -> failwith ("Invalid flags: " ^ String.concat ~sep:", " invalid)


let check_for_help (flags: string list) : bool =
  if (Stdlib.List.mem "-h" flags)|| (Stdlib.List.mem "-help" flags) || (Stdlib.List.mem "--help" flags) then (
    print_help_message (); true
  ) else false


let check_for_module_versions (flags: string list) : bool =
  if Stdlib.List.mem "-module-versions" flags then
    let () = Cwe_checker_core.Main.print_module_versions () in
    true
  else
    false


let check_for_no_logging (flags: string list) : unit =
  if Stdlib.List.mem "-no-logging" flags then
    Cwe_checker_core.Log_utils.turn_off_logging ()


let process_flags (flags : string list) : string list =
  match flags with
  | [] -> []
  | _  -> validate_user_input flags; flags


let rec check_for_binary_path (args : string list) : string =
  match args with
  | [] -> raise (NoBinaryPathException ("No binary path was provided. If you need help, please call the cwe_checker with the --help or -h flag"))
  | head::tail ->(
      try
        match Sys.is_directory head with
        | false -> head
        | true  -> raise (NoBinaryPathException ("No binary path was provided. If you need help, please call the cwe_checker with the --help or -h flag"))
      with
      | _ -> check_for_binary_path tail
   )


let process_input () : string * string list =
  match get_user_input () with
  | [] -> raise (NoBinaryPathException ("No binary path was provided. If you need help, please call the cwe_checker with the --help or -h flag"))
  | input  -> (
      if check_for_help input then exit 0;
      if check_for_module_versions input then exit 0;
      check_for_no_logging input;
      let binary_path = check_for_binary_path input in
      let split_flags = List.partition_tf input ~f:(fun x -> (String.is_prefix x ~prefix:"-config") || (String.is_prefix x ~prefix:"-out") || (String.is_prefix x ~prefix:"-partial")) in
      let flags = remove_element (snd split_flags) binary_path in
      let params = fst split_flags in
      check_partial params; check_config params; check_output_path params;
      (binary_path, params @ process_flags flags)
    )


let main () : int =
  match Array.length Sys.argv with
  | 1 -> print_help_message (); 0
  | _ ->
      let args = process_input () in
      match snd args with
      | [] -> Sys.command ("bap " ^ fst args ^ " --pass=cwe-checker ")
      | _  -> Sys.command ("bap " ^ fst args ^ " --pass=cwe-checker " ^ setup_flags (snd args))


let _ = exit (main ())
