(*
 * Copyright (c) 2016 - 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
open! PVariant
module F = Format
module L = Logging

module GlobalsAccesses = PrettyPrintable.MakePPSet (struct
  type t = Pvar.t * Location.t

  let compare (v1, l1) (v2, l2) =
    (* compare by loc first to present reports in the right order *)
    [%compare : Location.t * Pvar.t] (l1, v1) (l2, v2)

  let pp fmt (v, _) =
    F.fprintf fmt "%a|%a" Mangled.pp (Pvar.get_name v) Pvar.pp_translation_unit
      (Pvar.get_translation_unit v)
end)

module TraceElem = struct
  module Kind = GlobalsAccesses

  type t = {site: CallSite.t; kind: [`Call | `Access] * Kind.t} [@@deriving compare]

  let call_site {site} = site

  let kind {kind} = snd kind

  let make ?indexes:_ kind site = {kind= (`Call, kind); site}

  let with_callsite {kind= _, kind} site = {kind= (`Call, kind); site}

  let pp fmt {site; kind} =
    F.fprintf fmt "%saccess to %a"
      (match fst kind with `Call -> "indirect " | `Access -> "")
      Kind.pp (snd kind) ;
    match fst kind with `Call -> F.fprintf fmt " at %a" CallSite.pp site | `Access -> ()

  module Set = PrettyPrintable.MakePPSet (struct
    (* Don't use nonrec due to https://github.com/janestreet/ppx_compare/issues/2 *)
    (* type nonrec t = t [@@deriving compare]; *)
    type nonrec t = t

    let compare = compare

    let pp = pp
  end)
end

include SinkTrace.Make (TraceElem)

let make_access kind loc =
  let site = CallSite.make Typ.Procname.empty_block loc in
  {TraceElem.kind= (`Access, kind); site}

let is_intraprocedural_access {TraceElem.kind= kind, _} = kind = `Access

let trace_of_error loc gname path =
  let desc_of_sink sink =
    let callsite = Sink.call_site sink in
    if is_intraprocedural_access sink then Format.asprintf "%a" Sink.pp sink
    else Format.asprintf "call to %a" Typ.Procname.pp (CallSite.pname callsite)
  in
  let sink_should_nest sink = not (is_intraprocedural_access sink) in
  let trace_elem_of_global =
    Errlog.make_trace_element 0 loc (Format.asprintf "initialization of %s" gname) []
  in
  let trace =
    let trace_with_set_of_globals = to_sink_loc_trace ~desc_of_sink ~sink_should_nest path in
    (* the last element of the trace gotten by [to_sink_loc_trace] contains a set of procedure-local
       accesses to globals. We want to remove it in exchange for as many trace elems as there are
       accesses. *)
    match (List.rev trace_with_set_of_globals, snd path) with
    | telem :: rest, ({TraceElem.kind= `Access, globals}, _) :: _
     -> let nesting = telem.Errlog.lt_level in
        let add_trace_elem_of_access err_trace (global, loc) =
          Errlog.make_trace_element nesting loc
            (Format.asprintf "access to %a" Mangled.pp (Pvar.get_name global))
            []
          :: err_trace
        in
        GlobalsAccesses.elements globals |> List.fold ~f:add_trace_elem_of_access ~init:rest
        |> List.rev
    | _
     -> trace_with_set_of_globals
  in
  trace_elem_of_global :: trace
