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
exception NoApiFileException 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 raise_no_content_exception (param : string) : unit =
  match param with
  | "-config" -> raise (NoConfigException "No config file provided. If -config flag set please provide a config file.")
  | "-out" -> raise (NoOutputFileException "No output file provided. If -out flag is set please provide an out file.")
  | "-partial" -> raise (NoModulesException "No modules provided. If -partial flag is set, please provide the corresponding modules.")
  | "-api" -> raise (NoApiFileException "No header file provided. If -api flag is set, please provide a valid header file.")
  | _ -> failwith "Invalid param."


let check_content (input : string) (param : string) : unit =
  match Stdlib.List.nth_opt (String.split input ~on:'=') 1 with
  | None | Some "" -> raise_no_content_exception param
  | Some content -> begin
      match param with
      | "-partial" -> check_valid_module_list (String.split_on_chars content ~on:[','])
      | "-config" | "-api" -> if (Sys.file_exists content) then () else raise (InvalidPathException "Path to config file not valid")
      | _ -> ()
    end


let check_params (params : string list) (input : string list) : unit =
  List.iter params ~f:(fun param ->
    match find_prefix input param with
    | None -> begin
        match (String.equal param "-config") with
        | true -> Cwe_checker_core.Log_utils.info "Using standard configuration..."
        | false -> ()
      end
    | Some p -> check_content p param
  )


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_version (flags:string list) : bool =
  if (Stdlib.List.mem "-v" flags) || (Stdlib.List.mem "-version" flags) || (Stdlib.List.mem "--version" flags) then (
    print_version (); 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 (() : unit) : 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_version 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") || (String.is_prefix x ~prefix:"-api")) in
      let flags = remove_element (snd split_flags) binary_path in
      let input_params = fst split_flags in
      let params = List.map cmdline_params ~f:(fun param -> match param with | (p, _) -> "-" ^ p) in
      check_params params input_params;
      (binary_path, input_params @ process_flags flags)
    )


let setup_command (bin_path : string) (args : string list) : string =
  let bare_command = "bap " ^ bin_path ^ " --pass=cwe-checker " in
  let command_args = String.concat ~sep:" " (List.map args ~f:(fun arg ->
    match (String.is_prefix arg ~prefix:"-api") with
    | true -> "--api-path=" ^ (Stdlib.List.nth (String.split arg ~on:'=') 1)
    | false -> "--cwe-checker" ^ arg)) in
  bare_command ^ command_args



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


let _ = exit (main ())