(*
 * Copyright (c) 2018-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *)

open! IStd
module L = Logging
module F = Format
module JPS = JavaProfilerSamples

let load_all_statement =
  ResultsDatabase.register_statement
    "SELECT DISTINCT proc_name_hum FROM procedures WHERE modified_flag = 1 ORDER BY proc_name_hum"


let load_all () =
  ResultsDatabase.with_registered_statement load_all_statement ~f:(fun db stmt ->
      Container.to_list stmt
        ~fold:
          (SqliteUtils.result_fold_single_column_rows ~finalize:false db
             ~log:"modified_flag.load_all")
      |> List.map ~f:Sqlite3.Data.to_string )


let store_statement =
  ResultsDatabase.register_statement
    "UPDATE procedures SET modified_flag = :i WHERE proc_name = :k"


let store pname modified =
  let modified' = Sqlite3.Data.INT (Int64.of_int (if modified then 1 else 0)) in
  ResultsDatabase.with_registered_statement store_statement ~f:(fun db stmt ->
      Typ.Procname.SQLite.serialize pname
      |> Sqlite3.bind stmt 2
      |> SqliteUtils.check_result_code db ~log:"store bind proc name" ;
      modified' |> Sqlite3.bind stmt 1
      |> SqliteUtils.check_result_code db ~log:"store bind modified flag " ;
      SqliteUtils.result_unit db ~finalize:false ~log:"store modified flag " stmt )


(* a flag used to make the method search signature sensitive *)
let use_method_signature = false

module RangeMap = Caml.Map.Make (struct
  type t = Typ.Procname.t

  let compare = Typ.Procname.compare
end)

let initialized_test_determinator = ref false

let is_test_determinator_init () = !initialized_test_determinator

module MethodRangeMap = struct
  let map : (Location.t * Location.t) RangeMap.t ref = ref RangeMap.empty

  let method_range_map () = !map

  let split_class_method_name qualified_method_name =
    String.rsplit2_exn qualified_method_name ~on:'.'


  let create_java_method_range_map code_graph_file' =
    match code_graph_file' with
    | Some code_graph_file ->
        let open Java_method_decl_j in
        let json_string =
          match Utils.read_file code_graph_file with
          | Ok cl_list ->
              let json = List.fold cl_list ~init:"" ~f:(fun acc s -> acc ^ s) in
              json
          | Error _ ->
              L.die UserError "Could not read file %s" code_graph_file
        in
        let method_decls = java_method_decls_of_string json_string in
        let map' =
          List.fold method_decls ~init:RangeMap.empty ~f:(fun acc decl ->
              let start_location =
                { Location.line= decl.start_line
                ; col= -1
                ; file= SourceFile.create ~warn_on_error:false decl.source_file }
              in
              let end_location =
                { Location.line= decl.end_line
                ; col= -1
                ; file= SourceFile.create ~warn_on_error:false decl.source_file }
              in
              let range = (start_location, end_location) in
              let classname, methodname = split_class_method_name decl.method_name in
              match decl.signature with
              | Some signature ->
                  let signature =
                    if use_method_signature then signature
                    else
                      (* When we should not use the signature we use 'void ()' *)
                      JPS.JNI.void_method_with_no_arguments
                  in
                  let key = JPS.create_procname ~classname ~methodname ~signature in
                  RangeMap.add key range acc
              | None ->
                  acc )
        in
        map := map'
    | _ ->
        L.die UserError "Missing method declaration info argument"


  let create_clang_method_range_map cfg =
    let update_method_range_map pdesc =
      let start_node = Procdesc.get_start_node pdesc in
      let exit_node = Procdesc.get_exit_node pdesc in
      let range = (Procdesc.Node.get_loc start_node, Procdesc.Node.get_loc exit_node) in
      let key = Procdesc.get_proc_name pdesc in
      map := RangeMap.add key range !map
    in
    Typ.Procname.Hash.iter (fun _ pdesc -> update_method_range_map pdesc) cfg


  let pp_map fmt () =
    let pp_map' fmt m =
      RangeMap.iter
        (fun key range ->
          F.fprintf fmt "@\n      %a --> (%a,%a)" Typ.Procname.pp key Location.pp (fst range)
            Location.pp (snd range) )
        m
    in
    F.fprintf fmt "@\n--- Method Range Map ---%a@\n--- End Method Range Map --- @\n" pp_map' !map
end

module DiffLines = struct
  (* This is a map
        file name |--> {set of changed line }
  *)
  let map : int list String.Map.t ref = ref String.Map.empty

  let changed_lines_map () = !map

  (* Read the file containing info on changed lines and populate the map *)
  let init_changed_lines_map changed_lines_file' =
    match changed_lines_file' with
    | Some changed_lines_file -> (
        L.(debug TestDeterminator Medium)
          "Initializing changed lines map from file '%s'..." changed_lines_file ;
        match Utils.read_file changed_lines_file with
        | Ok cl_list ->
            let changed_lines =
              List.fold cl_list ~init:String.Map.empty ~f:(fun acc cl_item ->
                  let fname, cl = String.rsplit2_exn ~on:':' cl_item in
                  String.Map.set acc ~key:fname ~data:(FileDiff.parse_unix_diff cl) )
            in
            L.(debug TestDeterminator Medium) "done@\n" ;
            map := changed_lines
        | Error _ ->
            L.die UserError "Could not read file %s" changed_lines_file )
    | _ ->
        L.die UserError "Missing modified lines argument"


  let print_changed_lines () =
    L.(debug TestDeterminator Quiet) "--- Changed Lines Map ---@\n" ;
    String.Map.iteri !map ~f:(fun ~key ~data ->
        L.(debug TestDeterminator Quiet)
          "%s --> [%a]@\n" key (Pp.seq ~sep:", " F.pp_print_int) data )
end

let pp_profiler_sample_set fmt s =
  F.fprintf fmt " (set size = %i) " (JPS.ProfilerSample.cardinal s) ;
  JPS.ProfilerSample.iter (fun m -> F.fprintf fmt "@\n      <Method:>  %a " Typ.Procname.pp m) s


module TestSample = struct
  let labeled_test_samples = ref []

  let test_sample () = !labeled_test_samples

  let init_test_sample test_samples_file' =
    match test_samples_file' with
    | Some test_samples_file ->
        L.(debug TestDeterminator Medium)
          "Reading Profiler Samples File '%s'....@\n" test_samples_file ;
        let ts = JPS.from_json_file test_samples_file ~use_signature:use_method_signature in
        labeled_test_samples := ts
    | _ ->
        L.die UserError "Missing profiler samples argument"


  let pp_map fmt () =
    List.iter !labeled_test_samples ~f:(fun (label, profiler_samples) ->
        F.fprintf fmt "=== Samples for %s ===@\n%a@\n=== End Samples for %s ===@\n" label
          pp_profiler_sample_set profiler_samples label )
end

let in_range l range = l >= (fst range).Location.line && l <= (snd range).Location.line

let affected_methods method_range_map file_changed_lines changed_lines =
  L.(debug TestDeterminator Medium) "Looking for affected methods in file '%s' " file_changed_lines ;
  RangeMap.fold
    (fun key ((l1, _) as range) acc ->
      let method_file = SourceFile.to_string l1.Location.file in
      if
        String.equal method_file file_changed_lines
        && List.exists ~f:(fun l -> in_range l range) changed_lines
      then (
        L.(debug TestDeterminator Medium)
          "Adding '%a' in affected methods...@\n" Typ.Procname.pp key ;
        JPS.ProfilerSample.add key acc )
      else acc )
    method_range_map JPS.ProfilerSample.empty


let compute_affected_methods_java changed_lines_map method_range_map =
  let affected_methods =
    String.Map.fold changed_lines_map ~init:JPS.ProfilerSample.empty
      ~f:(fun ~key:file_changed_lines ~data acc ->
        let am = affected_methods method_range_map file_changed_lines data in
        JPS.ProfilerSample.union am acc )
  in
  L.(debug TestDeterminator Quiet)
    "== Affected Methods ==%a@\n== End Affected Methods ==@\n" pp_profiler_sample_set
    affected_methods ;
  affected_methods


let compute_affected_methods_clang source_file changed_lines_map method_range_map =
  let fname = SourceFile.to_rel_path source_file in
  L.(debug TestDeterminator Medium) "Looking for file %s in changed-line map..." fname ;
  match String.Map.find changed_lines_map fname with
  | Some changed_lines ->
      L.(debug TestDeterminator Medium) "found!@\n" ;
      let affected_methods = affected_methods method_range_map fname changed_lines in
      L.(debug TestDeterminator Medium)
        "@\n@\n== Resulting Affected Methods ==%a@\n== End Affected Methods ==@\n\n"
        pp_profiler_sample_set affected_methods ;
      affected_methods
  | None ->
      L.(debug TestDeterminator Medium)
        "%s not found in changed-line map. Nothing else to do for it.@\n" fname ;
      JPS.ProfilerSample.empty


let relevant_tests = ref []

(* Methods modified in a diff *)
let relevant_methods = ref JPS.ProfilerSample.empty

let _get_relevant_test_to_run () = !relevant_tests

let emit_tests_to_run () =
  let json = `List (List.map ~f:(fun t -> `String t) !relevant_tests) in
  let outpath = Config.results_dir ^/ "test_determinator.json" in
  Yojson.Basic.to_file outpath json ;
  L.(debug TestDeterminator Medium)
    "Tests to run: [%a]@\n"
    (Pp.seq ~sep:", " F.pp_print_string)
    !relevant_tests


let persist_relevant_method_in_db () =
  JPS.ProfilerSample.iter (fun pname -> store pname true) !relevant_methods


let emit_relevant_methods () =
  let rel_methods = load_all () in
  let json = `List (List.map ~f:(fun t -> `String t) rel_methods) in
  let outpath = Config.results_dir ^/ "diff_determinator.json" in
  Yojson.Basic.to_file outpath json ;
  L.(debug TestDeterminator Medium)
    "Methods modified in this Diff: %a@\n"
    (Pp.seq ~sep:", " F.pp_print_string)
    rel_methods


let init_clang cfg changed_lines_file test_samples_file =
  DiffLines.init_changed_lines_map changed_lines_file ;
  DiffLines.print_changed_lines () ;
  MethodRangeMap.create_clang_method_range_map cfg ;
  L.(debug TestDeterminator Medium) "%a@\n" MethodRangeMap.pp_map () ;
  match test_samples_file with
  | Some _ ->
      TestSample.init_test_sample test_samples_file
  | _ ->
      () ;
      L.(debug TestDeterminator Medium) "%a@\n" TestSample.pp_map () ;
      initialized_test_determinator := true


let init_java changed_lines_file test_samples_file code_graph_file =
  DiffLines.init_changed_lines_map changed_lines_file ;
  DiffLines.print_changed_lines () ;
  MethodRangeMap.create_java_method_range_map code_graph_file ;
  L.(debug TestDeterminator Medium) "%a@\n" MethodRangeMap.pp_map () ;
  TestSample.init_test_sample test_samples_file ;
  L.(debug TestDeterminator Medium) "%a@\n" TestSample.pp_map () ;
  initialized_test_determinator := true


(* test_to_run = { n | Affected_Method /\ ts_n != 0 } *)
let test_to_run_clang source_file cfg changed_lines_file test_samples_file =
  L.(debug TestDeterminator Quiet)
    "****** Start Test Determinator for  %s *****@\n"
    (SourceFile.to_string source_file) ;
  if is_test_determinator_init () then () else init_clang cfg changed_lines_file test_samples_file ;
  let affected_methods =
    compute_affected_methods_clang source_file (DiffLines.changed_lines_map ())
      (MethodRangeMap.method_range_map ())
  in
  relevant_methods := JPS.ProfilerSample.union affected_methods !relevant_methods ;
  let test_to_run =
    if JPS.ProfilerSample.is_empty affected_methods then []
    else
      List.fold (TestSample.test_sample ()) ~init:[] ~f:(fun acc (label, profiler_samples) ->
          let intersection = JPS.ProfilerSample.inter affected_methods profiler_samples in
          if JPS.ProfilerSample.is_empty intersection then acc else label :: acc )
  in
  relevant_tests := List.append test_to_run !relevant_tests


let test_to_run_java changed_lines_file test_samples_file code_graph_file =
  L.(debug TestDeterminator Quiet) "***** Start Java Test Determinator *****@\n" ;
  if is_test_determinator_init () then ()
  else init_java changed_lines_file test_samples_file code_graph_file ;
  let affected_methods =
    compute_affected_methods_java (DiffLines.changed_lines_map ())
      (MethodRangeMap.method_range_map ())
  in
  let test_to_run =
    if JPS.ProfilerSample.is_empty affected_methods then []
    else
      List.fold (TestSample.test_sample ()) ~init:[] ~f:(fun acc (label, profiler_samples) ->
          let intersection = JPS.ProfilerSample.inter affected_methods profiler_samples in
          if JPS.ProfilerSample.is_empty intersection then acc
          else (
            L.(debug TestDeterminator Quiet)
              "Choosing test '%s' because of [%a]@\n" label
              (Pp.seq Typ.Procname.pp ~sep:", ")
              (JPS.ProfilerSample.elements intersection) ;
            label :: acc ) )
  in
  relevant_tests := List.append test_to_run !relevant_tests
