(*
 * 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 F = Format
module L = Logging

module Reverse = struct
  module Dead = AbstractDomain.BooleanAnd
  module VarSet = AbstractDomain.FiniteSet (Var)
  module DeadAndVarSet = AbstractDomain.Pair (Dead) (VarSet)
  module M = AbstractDomain.Map (Var) (DeadAndVarSet)

  (* invariant: sets are not empty *)
  type t = M.t

  let empty = M.empty

  let add k v rm =
    M.update k
      (function
        | None -> Some (false, VarSet.singleton v) | Some (_, s) -> Some (false, VarSet.add v s) )
      rm


  let mem = M.mem

  let deadify k rm =
    M.update k
      (function (None | Some (true, _)) as vo -> vo | Some (false, s) -> Some (true, s))
      rm


  let remove k v rm =
    match M.find_opt k rm with
    | None ->
        (rm, false)
    | Some (dead, s) ->
        let s = VarSet.remove v s in
        if VarSet.is_empty s then (M.remove k rm, dead) else (M.add k (dead, s) rm, false)


  let pp = M.pp

  let join = M.join

  let widen = M.widen
end

type t =
  { resolve: IdAccessPathMapDomain.t
  ; reverse: Reverse.t
        (** there is a [x -> y] mapping in reverse for each variable [y] appearing in [resolve x] *)
  }

let empty = {resolve= IdAccessPathMapDomain.empty; reverse= Reverse.empty}

let add id ap {resolve; reverse} =
  if (Config.debug_exceptions || Config.write_html) && Reverse.mem id reverse then
    (if Config.debug_exceptions then L.(die InternalError) else L.d_printfln ?color:None)
      "Variable %a appearing on both sides of bindings" Var.pp id ;
  let resolve = IdAccessPathMapDomain.add id ap resolve in
  let reverse =
    HilExp.AccessExpression.fold_vars ap ~init:reverse ~f:(fun acc var_in_ap ->
        Reverse.add var_in_ap id acc )
  in
  {resolve; reverse}


let exit_scope id bindings =
  let {resolve; reverse} = bindings in
  match IdAccessPathMapDomain.find_opt id resolve with
  | None ->
      if Reverse.mem id reverse then
        let reverse = Reverse.deadify id reverse in
        ({resolve; reverse}, [])
      else (bindings, [id])
  | Some ap ->
      let resolve = IdAccessPathMapDomain.remove id resolve in
      let reverse, vars =
        HilExp.AccessExpression.fold_vars ap ~init:(reverse, [])
          ~f:(fun (reverse, vars) var_in_ap ->
            let reverse, dead = Reverse.remove var_in_ap id reverse in
            let vars = if dead then var_in_ap :: vars else vars in
            (reverse, vars) )
      in
      ({resolve; reverse}, vars)


let resolve {resolve} id = IdAccessPathMapDomain.find_opt id resolve

let fold {resolve} ~init ~f = IdAccessPathMapDomain.fold f resolve init

let pp f {resolve; reverse} =
  F.fprintf f "{@[<v1> resolve=@[<hv>%a@];@;reverse=@[<hv>%a@]@]}" IdAccessPathMapDomain.pp resolve
    Reverse.pp reverse


let ( <= ) ~lhs ~rhs = IdAccessPathMapDomain.( <= ) ~lhs:lhs.resolve ~rhs:rhs.resolve

let join bindings1 bindings2 =
  if phys_equal bindings1 bindings2 then bindings1
  else
    let {resolve= resolve1; reverse= reverse1} = bindings1 in
    let {resolve= resolve2; reverse= reverse2} = bindings2 in
    {resolve= IdAccessPathMapDomain.join resolve1 resolve2; reverse= Reverse.join reverse1 reverse2}


let widen ~prev ~next ~num_iters =
  if phys_equal prev next then prev
  else
    let {resolve= resolve1; reverse= reverse1} = prev in
    let {resolve= resolve2; reverse= reverse2} = next in
    { resolve= IdAccessPathMapDomain.widen ~prev:resolve1 ~next:resolve2 ~num_iters
    ; reverse= Reverse.widen ~prev:reverse1 ~next:reverse2 ~num_iters }
