module MethodUnifier

open Ast
open Getters
open AstUtils
open FixpointSolver
open PrintUtils
open Resolver
open Utils

let TryUnify targetMthd candMethod =
  let targetPre,targetPost = GetMethodPrePost targetMthd
  let targetPre = BinaryAnd targetPre (GetMethodGhostPrecondition targetMthd)
  let candPre,candPost = GetMethodPrePost candMethod
  let candPre = BinaryAnd candPre (GetMethodGhostPrecondition candMethod)
  let builder = new CascadingBuilder<_>(None)
  builder {
    let! unifs1 = UnifyImplies targetPre candPre RTL Map.empty
    let! unifs2 = UnifyImplies candPost targetPost LTR unifs1
    return Some(unifs2)
  }

let rec TryFindAMatch targetMthd candidateMethods =
  let targetMthdName = GetMethodName targetMthd
  match candidateMethods with
  | candMthd :: rest ->
      if GetMethodName candMthd = targetMthdName then
        // skip if it is the same method
        TryFindAMatch targetMthd rest
      else
        match TryUnify targetMthd candMthd with
        | Some(unifs) -> Some(candMthd,unifs)
        | None -> TryFindAMatch targetMthd rest
  | [] -> None

let TryFindExistingOpt comp targetMthd =
  TryFindAMatch targetMthd (GetMembers comp |> FilterMethodMembers)

let TryFindExisting comp targetMthd =
  match TryFindAMatch targetMthd (GetMembers comp |> FilterMethodMembers) with
  | Some(m,unifs) -> m,unifs
  | None -> targetMthd, Map.empty

let ApplyMethodUnifs receiver (c,m) unifs =
  let __Apply args = args |> List.map (fun var ->
                                         let name = GetExtVarName var
                                         match Map.tryFind name unifs with
                                         | Some(e) -> e
                                         | None -> VarLiteral(name))
  let ins = GetMethodInArgs m |> __Apply
  let outs = GetMethodOutArgs m |> __Apply

  let retVars, asgs = outs |> List.fold (fun (acc1,acc2) e ->
                                          let vname = SymGen.NewSymFake e
                                          let v = Var(vname, None, false)
                                          let acc1' = acc1 @ [v]
                                          let acc2' = acc2 @ [ArbitraryStatement(Assign(VarLiteral(vname), e))]
                                          acc1', acc2'
                                       ) ([],[])
  let mcallExpr = MethodCall(receiver, GetComponentName c, GetMethodName m, ins)
  match retVars, outs with
  | [], [] -> [ArbitraryStatement(ExprStmt(mcallExpr))]
  | [_], [VarLiteral(vn2)] -> [ArbitraryStatement(Assign(VarDeclExpr([Var(vn2, None, false)], false), mcallExpr))]
  | _ ->
      let mcall = ArbitraryStatement(Assign(VarDeclExpr(retVars, true), mcallExpr))
      mcall :: asgs

//  ====================================================
///
//  ====================================================
let TryFindExistingAndConvertToSolution indent comp m cond callGraph =
  let __Calls caller callee =
    let keyOpt = callGraph |> Map.tryFindKey (fun (cc,mm) mset -> CheckSameMethods (comp,caller) (cc,mm))
    match keyOpt with
    | Some(k) -> callGraph |> Map.find k |> Set.contains ((GetComponentName comp),(GetMethodName callee))
    | None -> false
  (* --- function body starts here --- *)
  if not Options.CONFIG.genMod then
    None
  else
    let idt = Indent indent
    let candidateMethods = GetMembers comp |> List.filter (fun cm ->
                                                             match cm with
                                                             | Method(mname,_,_,_,_) when not (__Calls cm m) -> true
                                                             | _ -> false)
    match TryFindAMatch m candidateMethods with
    | Some(m',unifs) ->
        Logger.InfoLine (idt + "    - substitution method found:")
        Logger.InfoLine (Printer.PrintMethodSignFull (indent+6) comp m')
        Logger.DebugLine (idt + "      Unifications: ")
        let idtt = idt + "        "
        unifs |> Map.fold (fun acc k v -> acc + (sprintf "%s%s -> %s%s" idtt k (Printer.PrintExpr 0 v) newline)) "" |> Logger.Debug
        let obj = { name = "this"; objType = GetClassType comp }
        let modObjs = if IsModifiableObj obj (comp,m) then Set.singleton obj else Set.empty
        let body = ApplyMethodUnifs ThisLiteral (comp,m') unifs
        let hInst = { objs           = Utils.MapSingleton obj.name obj;
                      modifiableObjs         = modObjs;
                      assignments            = body;
                      concreteValues         = body;
                      methodArgs             = Map.empty;
                      methodRetVals          = Map.empty;
                      concreteMethodRetVals  = Map.empty;
                      globals                = Map.empty }
        Some(Map.empty |> Map.add (comp,m) [cond, hInst]
                       |> Map.add (comp,m') [])
    | None -> None

