module Modularizer

open Ast
open Getters
open AstUtils
open MethodUnifier
open PrintUtils
open Resolver
open Utils

//  =======================================================================
/// Merges two solution maps so that if there are multiple entries for a
/// single (comp,method) pair it concatenates them (corresponds to multiple
/// branches).
//  =======================================================================
let MergeSolutions sol1 sol2 =
  let rec __Merge sol1map sol2lst res =
    match sol2lst with
    | ((c2,m2), lst2) :: rest ->
        match sol1map |> Map.tryFindKey (fun (c1,m1) lst1 -> CheckSameMethods (c1,m1) (c2,m2)) with
        | Some(c1,m1) ->
            let lst1 = sol1map |> Map.find(c1,m1)
            let newRes = res |> Map.add (c1,m1) (lst1@lst2)
            __Merge sol1map rest newRes
        | None ->
            let newRes = res |> Map.add (c2,m2) lst2
            __Merge sol1map rest newRes
    | [] -> res
  (* --- function body starts here --- *)
  __Merge sol1 (sol2 |> Map.toList) sol1

//  ===========================================
///
//  ===========================================
let rec MakeModular indent prog comp meth cond hInst callGraph =
  let directChildren = lazy (GetDirectModifiableChildren hInst)

  let __IsAbstractField ty var =
    let builder = CascadingBuilder<_>(false)
    let varName = GetVarName var
    builder {
      let! comp = FindComponent prog (GetTypeShortName ty)
      let! fld = GetAbstractFields comp |> List.fold (fun acc v -> if GetVarName v = varName then Some(varName) else acc) None
      return true
    }

  let __FindObj objName =
    try
      //hInst.assignments |> List.find (fun ((obj,_),_) -> obj.name = objName) |> fst |> fst
      hInst.assignments |> List.choose (function FieldAssignment((obj,_),_) ->
                                                   if (obj.name = objName) then Some(obj) else None
                                                 | _ -> None)
                        |> List.head
    with
      | ex -> failwithf "obj %s not found for method %s" objName (GetMethodFullName comp meth)

  let __GetObjLitType objLitName =
    (__FindObj objLitName).objType

  //  ===============================================================================
  /// Goes through the assignments of the heapInstance and returns only those
  /// assignments that correspond to abstract fields of the given "objLitName" object
  //  ===============================================================================
  let __GetAbsFldAssignments objLitName =
    hInst.assignments |> List.choose (function
                                        FieldAssignment ((obj,var),e) ->
                                          if obj.name = objLitName && __IsAbstractField obj.objType var then
                                            Some(var,e)
                                          else
                                            None
                                        | _ -> None)

  //  ===============================================================================
  /// The given assignment is:
  ///   x := e
  ///
  /// If e is an object (e.g. gensym32) with e.g. two abstract fields "a" and "b",
  /// with values 3 and 8 respectively, then the "x := e" spec is fixed as following:
  ///   x.a := 3 && x.b := 8
  ///
  /// List values are handled similarly, e.g.:
  ///   x := [gensym32]
  /// is translated into
  ///   |x| = 1 && x[0].a = 3 && x[0].b = 8
  //  ===============================================================================
  let rec __ExamineAndFix x e =
    match e with
    | ObjLiteral(id) when not (Utils.ListContains e (directChildren.Force())) -> //TODO: is it really only non-direct children?
        let absFlds = __GetAbsFldAssignments id
        absFlds |> List.fold (fun acc (var,vval) -> BinaryAnd acc (BinaryEq (Dot(x, GetVarName var)) vval)) TrueLiteral
    | SequenceExpr(elist) ->
        let rec __fff lst acc cnt =
          match lst with
          | fsExpr :: rest ->
              let acc = BinaryAnd acc (__ExamineAndFix (SelectExpr(x, IntLiteral(cnt))) fsExpr)
              __fff rest acc (cnt+1)
          | [] ->
              let lenExpr = BinaryEq (SeqLength(x)) (IntLiteral(cnt))
              BinaryAnd lenExpr acc
        __fff elist TrueLiteral 0
    | _ -> BinaryEq x e

  //  ================================================================================
  /// The spec for an object consists of assignments to its abstract fields with one
  /// caveat: if some assignments include non-direct children objects of "this", then
  /// those objects cannot be used directly in the spec; instead, their properties must
  /// be expanded and embeded (that's what the "ExamineAndFix" function does)
  //  ================================================================================
  let __GetSpecFor objLitName =
    let absFieldAssignments = __GetAbsFldAssignments objLitName
    let absFldAssgnExpr = absFieldAssignments |> List.fold (fun acc (var,e) -> BinaryAnd acc (__ExamineAndFix (IdLiteral(GetVarName var)) e)) TrueLiteral
    let retValExpr = hInst.methodRetVals |> Map.fold (fun acc varName varValueExpr -> BinaryAnd acc (BinaryEq (VarLiteral(varName)) varValueExpr)) TrueLiteral
    BinaryAnd absFldAssgnExpr retValExpr

  //  ================================================================================================
  /// Simply traverses a given expression and returns all arguments of the "meth" method that are used
  //  ================================================================================================
  let __GetArgsUsed expr =
    let args = GetMethodArgs meth
    let argSet = DescendExpr2 (fun e acc ->
                                 match e with
                                 | VarLiteral(vname) ->
                                     match args |> List.tryFind (fun var -> GetVarName var = vname) with
                                     | Some(var) -> acc |> Set.add var
                                     | None -> acc
                                 | _ -> acc
                               ) expr Set.empty
    argSet |> Set.toList

  let rec __GetDelegateMethods objs acc =
    match objs with
    | ObjLiteral(name) as obj :: rest ->
        let mName = sprintf "_synth_%s_%s" (GetMethodFullName comp meth |> String.map (fun c -> if c = '.' then '_' else c)) name
        let pre,_ = GetMethodPrePost meth //TrueLiteral
        let post = __GetSpecFor name
        let ins = __GetArgsUsed (BinaryAnd pre post)
        let sgn = Sig(ins, [])
        let m = Method(mName, sgn, pre, post, true)
        let c = FindComponent prog (name |> __GetObjLitType |> GetTypeShortName) |> Utils.ExtractOption
        let m',unifs = TryFindExisting c m
        let args = ApplyMethodUnifs obj (c,m') unifs
        __GetDelegateMethods rest (acc |> Map.add obj (c,m',args))
    | _ :: rest -> failwith "internal error: expected to see only ObjLiterals"
    | [] -> acc

  //  =======================================================================
  /// Tries to make a given solution for a given method into more modular,
  /// by delegating some statements (initialization of inner objects) to
  /// method calls.
  //  =======================================================================
  let __GetModularBranch =
    let delegateMethods = __GetDelegateMethods (directChildren.Force()) Map.empty
    let initChildrenExprList = delegateMethods |> Map.toList
                                               |> List.map (fun (_, (_,_,asgs)) -> asgs)
                                               |> List.concat
    let newAssgns = hInst.assignments |> List.filter (function FieldAssignment((obj,_),_) -> obj.name = "this" | _ -> false)
    let newMethodsLst = delegateMethods |> Map.fold (fun acc receiver (c,newMthd,_) ->
                                                       (c,newMthd) :: acc
                                                    ) []
    newMethodsLst, { hInst with assignments = initChildrenExprList @ newAssgns }

  (* --- function body starts here --- *)
  let idt = Indent indent
  if Options.CONFIG.genMod then
    Logger.InfoLine (idt + "    - delegating to method calls ...")
    // first try to find a match for the entire method (based on the given solution)
    let postSpec = __GetSpecFor "this"
    let meth' = match meth with
                | Method (mname, msig, mpre, _, isConstr) -> Method(mname, msig, mpre, postSpec, isConstr)
                | _ -> failwithf "internal error: expected a Method but got %O" meth
    match TryFindExistingAndConvertToSolution indent comp meth' cond callGraph with
    | Some(sol) -> sol |> FixSolution comp meth
    | None ->
        // if not found, try to split into parts
        let newMthdLst, newHeapInst = __GetModularBranch
        let msol = Utils.MapSingleton (comp,meth) [cond, newHeapInst]
        newMthdLst |> List.fold (fun acc (c,m) ->
                                   acc |> MergeSolutions (Utils.MapSingleton (c,m) [])
                                 ) msol
  else
    Utils.MapSingleton (comp,meth) [cond, hInst]

//let GetModularSol prog sol =
//  let comp = fst (fst sol)
//  let meth = snd (fst sol)
//  let rec __xxx prog lst =
//    match lst with
//    | (cond, hInst) :: rest ->
//        let newProg, newComp, newMthdLst, newhInst = GetModularBranch prog comp meth hInst
//        let newProg, newRest = __xxx newProg rest
//        newProg, ((cond, newhInst) :: newRest)
//    | [] -> prog, []
//  let newProg, newSolutions = __xxx prog (snd sol)
//  let newComp = FindComponent newProg (GetComponentName comp) |> Utils.ExtractOption
//  newProg, ((newComp, meth), newSolutions)
//
//let Modularize prog solutions =
//  let rec __Modularize prog sols acc =
//    match sols with
//    | sol :: rest ->
//        let (newProg, newSol) = GetModularSol prog sol
//        let newAcc = acc |> Map.add (fst newSol) (snd newSol)
//        __Modularize newProg rest newAcc
//    | [] -> (prog, acc)
//  (* --- function body starts here --- *)
//  __Modularize prog (Map.toList solutions) Map.empty
