(*
 * Copyright (c) 2015 - present Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *)

open! IStd
module Hashtbl = Caml.Hashtbl
module L = Logging
module F = Format

type node_tag =
  | Condition of bool
  | Exception of Typ.name
  | Procedure_start of Typ.Procname.t
  | Procedure_end of Typ.Procname.t

(** Element of a loc trace *)
type loc_trace_elem =
  { lt_level: int  (** nesting level of procedure calls *)
  ; lt_loc: Location.t  (** source location at the current step in the trace *)
  ; lt_description: string  (** description of the current step in the trace *)
  ; lt_node_tags: node_tag list  (** tags describing the node at the current location *) }

let pp_loc_trace_elem fmt {lt_level; lt_loc} = F.fprintf fmt "%d %a" lt_level Location.pp lt_loc

let pp_loc_trace fmt l = PrettyPrintable.pp_collection ~pp_item:pp_loc_trace_elem fmt l

let contains_exception loc_trace_elem =
  let pred nt =
    match nt with
    | Exception _
     -> true
    | Condition _ | Procedure_start _ | Procedure_end _
     -> false
  in
  List.exists ~f:pred loc_trace_elem.lt_node_tags

let make_trace_element lt_level lt_loc lt_description lt_node_tags =
  {lt_level; lt_loc; lt_description; lt_node_tags}

(** Trace of locations *)
type loc_trace = loc_trace_elem list

let compute_local_exception_line loc_trace =
  let compute_local_exception_line state step =
    match state with
    | `Stop _
     -> state
    | `Continue (last_known_step_at_level_zero_opt, line_opt)
     -> let last_known_step_at_level_zero_opt' =
          if Int.equal step.lt_level 0 then Some step else last_known_step_at_level_zero_opt
        in
        match last_known_step_at_level_zero_opt' with
        | Some step_zero when contains_exception step
         -> `Stop (last_known_step_at_level_zero_opt', Some step_zero.lt_loc.line)
        | _
         -> `Continue (last_known_step_at_level_zero_opt', line_opt)
  in
  snd (List_.fold_until ~init:(`Continue (None, None)) ~f:compute_local_exception_line loc_trace)

type node_id_key = {node_id: int; node_key: int}

type err_key =
  { err_kind: Exceptions.err_kind
  ; in_footprint: bool
  ; err_name: Localise.t
  ; err_desc: Localise.error_desc
  ; severity: string }
  [@@deriving compare]

(** Data associated to a specific error *)
type err_data =
  { node_id_key: node_id_key
  ; session: int
  ; loc: Location.t
  ; loc_in_ml_source: L.ml_loc option
  ; loc_trace: loc_trace
  ; err_class: Exceptions.err_class
  ; visibility: Exceptions.visibility
  ; linters_def_file: string option
  ; doc_url: string option }

let compare_err_data err_data1 err_data2 = Location.compare err_data1.loc err_data2.loc

module ErrDataSet = (* set err_data with no repeated loc *)
Caml.Set.Make (struct
  type t = err_data

  let compare = compare_err_data
end)

(** Hash table to implement error logs *)
module ErrLogHash = struct
  module Key = struct
    type t = err_key [@@deriving compare]

    (* NOTE: changing the hash function can change the order in which issues are reported. *)
    let hash key =
      Hashtbl.hash
        (key.err_kind, key.in_footprint, key.err_name, Localise.error_desc_hash key.err_desc)

    let equal key1 key2 =
      [%compare.equal : Exceptions.err_kind * bool * Localise.t]
        (key1.err_kind, key1.in_footprint, key1.err_name)
        (key2.err_kind, key2.in_footprint, key2.err_name)
      && Localise.error_desc_equal key1.err_desc key2.err_desc
  end

  include Hashtbl.Make (Key)
end

(** Type of the error log, to be reset once per function.
    Map err_kind, fotprint / re - execution flag, error name,
    error description, severity, to set of err_data. *)
type t = ErrDataSet.t ErrLogHash.t

let compare x y =
  let bindings x = ErrLogHash.fold (fun k d l -> (k, d) :: l) x [] in
  [%compare : (ErrLogHash.Key.t * ErrDataSet.t) list] (bindings x) (bindings y)

(** Empty error log *)
let empty () = ErrLogHash.create 13

(** type of the function to be passed to iter *)
type iter_fun = err_key -> err_data -> unit

(** Apply f to nodes and error names *)
let iter (f: iter_fun) (err_log: t) =
  ErrLogHash.iter
    (fun err_key set -> ErrDataSet.iter (fun err_data -> f err_key err_data) set)
    err_log

(** Return the number of elements in the error log which satisfy [filter] *)
let size filter (err_log: t) =
  let count = ref 0 in
  ErrLogHash.iter
    (fun key err_datas ->
      if filter key.err_kind key.in_footprint then count := !count + ErrDataSet.cardinal err_datas)
    err_log ;
  !count

(** Print errors from error log *)
let pp_errors fmt (errlog: t) =
  let f key _ =
    if Exceptions.equal_err_kind key.err_kind Exceptions.Kerror then
      F.fprintf fmt "%a@ " Localise.pp key.err_name
  in
  ErrLogHash.iter f errlog

(** Print warnings from error log *)
let pp_warnings fmt (errlog: t) =
  let f key _ =
    if Exceptions.equal_err_kind key.err_kind Exceptions.Kwarning then
      F.fprintf fmt "%a %a@ " Localise.pp key.err_name Localise.pp_error_desc key.err_desc
  in
  ErrLogHash.iter f errlog

(** Print an error log in html format *)
let pp_html source path_to_root fmt (errlog: t) =
  let pp_eds fmt err_datas =
    let pp_nodeid_session_loc fmt err_data =
      Io_infer.Html.pp_session_link source path_to_root fmt
        (err_data.node_id_key.node_id, err_data.session, err_data.loc.Location.line)
    in
    ErrDataSet.iter (pp_nodeid_session_loc fmt) err_datas
  in
  let pp_err_log do_fp ek key err_datas =
    if Exceptions.equal_err_kind key.err_kind ek && Bool.equal do_fp key.in_footprint then
      F.fprintf fmt "<br>%a %a %a" Localise.pp key.err_name Localise.pp_error_desc key.err_desc
        pp_eds err_datas
  in
  F.fprintf fmt "%aERRORS DURING FOOTPRINT@\n" Io_infer.Html.pp_hline () ;
  ErrLogHash.iter (pp_err_log true Exceptions.Kerror) errlog ;
  F.fprintf fmt "%aERRORS DURING RE-EXECUTION@\n" Io_infer.Html.pp_hline () ;
  ErrLogHash.iter (pp_err_log false Exceptions.Kerror) errlog ;
  F.fprintf fmt "%aWARNINGS DURING FOOTPRINT@\n" Io_infer.Html.pp_hline () ;
  ErrLogHash.iter (pp_err_log true Exceptions.Kwarning) errlog ;
  F.fprintf fmt "%aWARNINGS DURING RE-EXECUTION@\n" Io_infer.Html.pp_hline () ;
  ErrLogHash.iter (pp_err_log false Exceptions.Kwarning) errlog ;
  F.fprintf fmt "%aINFOS DURING FOOTPRINT@\n" Io_infer.Html.pp_hline () ;
  ErrLogHash.iter (pp_err_log true Exceptions.Kinfo) errlog ;
  F.fprintf fmt "%aINFOS DURING RE-EXECUTION@\n" Io_infer.Html.pp_hline () ;
  ErrLogHash.iter (pp_err_log false Exceptions.Kinfo) errlog

(* I use string in case we want to display a different name to the user*)
let severity_to_str severity =
  match severity with
  | Exceptions.High
   -> "HIGH"
  | Exceptions.Medium
   -> "MEDIUM"
  | Exceptions.Low
   -> "LOW"

(** Add an error description to the error log unless there is
    one already at the same node + session; return true if added *)
let add_issue tbl err_key (err_datas: ErrDataSet.t) : bool =
  try
    let current_eds = ErrLogHash.find tbl err_key in
    if ErrDataSet.subset err_datas current_eds then false
    else (
      ErrLogHash.replace tbl err_key (ErrDataSet.union err_datas current_eds) ;
      true )
  with Not_found -> ErrLogHash.add tbl err_key err_datas ; true

(** Update an old error log with a new one *)
let update errlog_old errlog_new =
  ErrLogHash.iter (fun err_key l -> ignore (add_issue errlog_old err_key l)) errlog_new

let log_issue err_kind err_log loc (node_id, node_key) session ltr ?linters_def_file ?doc_url exn =
  let err_name, err_desc, ml_loc_opt, visibility, severity, force_kind, eclass =
    Exceptions.recognize_exception exn
  in
  let err_kind = match force_kind with Some err_kind -> err_kind | _ -> err_kind in
  let hide_java_loc_zero =
    (* hide java errors at location zero unless in -developer_mode *)
    not Config.developer_mode && Config.curr_language_is Config.Java
    && Int.equal loc.Location.line 0
  in
  let hide_memory_error =
    match Localise.error_desc_get_bucket err_desc with
    | Some bucket when String.equal bucket Mleak_buckets.ml_bucket_unknown_origin
     -> not Mleak_buckets.should_raise_leak_unknown_origin
    | _
     -> false
  in
  let log_it =
    Exceptions.equal_visibility visibility Exceptions.Exn_user
    || Config.developer_mode && Exceptions.equal_visibility visibility Exceptions.Exn_developer
  in
  if log_it && not hide_java_loc_zero && not hide_memory_error then
    let added =
      let node_id_key = {node_id; node_key} in
      let err_data =
        { node_id_key
        ; session
        ; loc
        ; loc_in_ml_source= ml_loc_opt
        ; loc_trace= ltr
        ; err_class= eclass
        ; visibility
        ; linters_def_file
        ; doc_url }
      in
      let err_key =
        { err_kind
        ; in_footprint= !Config.footprint
        ; err_name
        ; err_desc
        ; severity= severity_to_str severity }
      in
      add_issue err_log err_key (ErrDataSet.singleton err_data)
    in
    let should_print_now = match exn with Exceptions.Internal_error _ -> true | _ -> added in
    let print_now () =
      let ex_name, desc, ml_loc_opt, _, _, _, _ = Exceptions.recognize_exception exn in
      L.(debug Analysis Medium)
        "@\n%a@\n@?" (Exceptions.pp_err ~node_key loc err_kind ex_name desc ml_loc_opt) () ;
      if err_kind <> Exceptions.Kerror then
        let warn_str =
          let pp fmt =
            Format.fprintf fmt "%s %a" (Localise.to_issue_id err_name) Localise.pp_error_desc desc
          in
          F.asprintf "%t" pp
        in
        let d =
          match err_kind with
          | Exceptions.Kerror
           -> L.d_error
          | Exceptions.Kwarning
           -> L.d_warning
          | Exceptions.Kinfo | Exceptions.Kadvice | Exceptions.Klike
           -> L.d_info
        in
        d warn_str ; L.d_ln ()
    in
    if should_print_now then print_now ()

type err_log = t

(** Global per-file error table *)
module Err_table = struct
  type t = err_log

  let create = empty

  let count_err err_table err_name locs = ignore (add_issue err_table err_name locs)

  let table_size filter (err_table: t) = size filter err_table

  let pp_stats_footprint ekind fmt (err_table: err_log) =
    let err_name_map = ref String.Map.empty in
    (* map error name to count *)
    let count_err (err_name: Localise.t) n =
      let err_string = Localise.to_issue_id err_name in
      let count =
        try String.Map.find_exn !err_name_map err_string
        with Not_found -> 0
      in
      err_name_map := String.Map.add ~key:err_string ~data:(count + n) !err_name_map
    in
    let count key err_datas =
      if Exceptions.equal_err_kind ekind key.err_kind && key.in_footprint then
        count_err key.err_name (ErrDataSet.cardinal err_datas)
    in
    ErrLogHash.iter count err_table ;
    let pp ~key:err_string ~data:count = F.fprintf fmt " %s:%d" err_string count in
    String.Map.iteri ~f:pp !err_name_map

  module LocMap = Caml.Map.Make (struct
    type t = ErrDataSet.elt

    let compare = compare_err_data
  end)

  let print_err_table_details fmt err_table =
    let map_err_fp = ref LocMap.empty in
    let map_err_re = ref LocMap.empty in
    let map_warn_fp = ref LocMap.empty in
    let map_warn_re = ref LocMap.empty in
    let map_info = ref LocMap.empty in
    let map_advice = ref LocMap.empty in
    let map_likes = ref LocMap.empty in
    let add_err nslm key =
      let map =
        match (key.in_footprint, key.err_kind) with
        | true, Exceptions.Kerror
         -> map_err_fp
        | false, Exceptions.Kerror
         -> map_err_re
        | true, Exceptions.Kwarning
         -> map_warn_fp
        | false, Exceptions.Kwarning
         -> map_warn_re
        | _, Exceptions.Kinfo
         -> map_info
        | _, Exceptions.Kadvice
         -> map_advice
        | _, Exceptions.Klike
         -> map_likes
      in
      try
        let err_list = LocMap.find nslm !map in
        map := LocMap.add nslm ((key.err_name, key.err_desc) :: err_list) !map
      with Not_found -> map := LocMap.add nslm [(key.err_name, key.err_desc)] !map
    in
    let f err_name eds = ErrDataSet.iter (fun loc -> add_err loc err_name) eds in
    ErrLogHash.iter f err_table ;
    let pp ekind err_data fmt err_names =
      List.iter
        ~f:(fun (err_name, desc) ->
          Exceptions.pp_err ~node_key:err_data.node_id_key.node_key err_data.loc ekind err_name
            desc err_data.loc_in_ml_source fmt ())
        err_names
    in
    F.fprintf fmt "@.Detailed errors during footprint phase:@." ;
    LocMap.iter
      (fun nslm err_names -> F.fprintf fmt "%a" (pp Exceptions.Kerror nslm) err_names)
      !map_err_fp ;
    F.fprintf fmt "@.Detailed errors during re-execution phase:@." ;
    LocMap.iter
      (fun nslm err_names -> F.fprintf fmt "%a" (pp Exceptions.Kerror nslm) err_names)
      !map_err_re ;
    F.fprintf fmt "@.Detailed warnings during footprint phase:@." ;
    LocMap.iter
      (fun nslm err_names -> F.fprintf fmt "%a" (pp Exceptions.Kwarning nslm) err_names)
      !map_warn_fp ;
    F.fprintf fmt "@.Detailed warnings during re-execution phase:@." ;
    LocMap.iter
      (fun nslm err_names -> F.fprintf fmt "%a" (pp Exceptions.Kwarning nslm) err_names)
      !map_warn_re
end

type err_table = Err_table.t

(** Create an error table *)
let create_err_table = Err_table.create

(** Print an error log and add it to the global per-file table *)
let extend_table err_table err_log = ErrLogHash.iter (Err_table.count_err err_table) err_log

(** Size of the global per-file error table for the footprint phase *)
let err_table_size_footprint ekind =
  let filter ekind' in_footprint = Exceptions.equal_err_kind ekind ekind' && in_footprint in
  Err_table.table_size filter

(** Print stats for the global per-file error table *)
let pp_err_table_stats ekind = Err_table.pp_stats_footprint ekind

(** Print details of the global per-file error table *)
let print_err_table_details = Err_table.print_err_table_details
