/-
Copyright (c) 2020 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
import Std.ShareCommon
import Lean.MetavarContext
import Lean.Environment
import Lean.Util.FoldConsts

namespace Lean
namespace Closure

structure Context :=
(lctxInput : LocalContext)
(zeta      : Bool) -- if `true` let-variables are expanded

structure State :=
(mctx          : MetavarContext)
(lctxOutput    : LocalContext := {})
(ngen          : NameGenerator := { namePrefix := `_closure })
(visitedLevel  : LevelMap Level := {})
(visitedExpr   : ExprStructMap Expr := {})
(levelParams   : Array Name := #[])
(nextLevelIdx  : Nat := 1)
(levelClosure  : Array Level := #[])
(nextExprIdx   : Nat := 1)
(exprClosure   : Array Expr := #[])

def Exception := String

abbrev ClosureM := ReaderT Context (EStateM Exception State)

@[inline] def visitLevel (f : Level → ClosureM Level) (u : Level) : ClosureM Level :=
if !u.hasMVar && !u.hasParam then pure u
else do
  s ← get;
  match s.visitedLevel.find? u with
  | some v => pure v
  | none   => do
    v ← f u;
    modify $ fun s => { s with visitedLevel := s.visitedLevel.insert u v };
    pure v

def mkNewLevelParam (u : Level) : ClosureM Level := do
s ← get;
let p := (`u).appendIndexAfter s.nextLevelIdx;
modify $ fun s => { s with levelParams := s.levelParams.push p, nextLevelIdx := s.nextLevelIdx + 1, levelClosure := s.levelClosure.push u };
pure $ mkLevelParam p

def getMCtx : ClosureM MetavarContext := do
s ← get; pure s.mctx

def instantiateMVars (e : Expr) : ClosureM Expr := do
modifyGet fun s =>
  let (e, mctx) := s.mctx.instantiateMVars e;
  (e, { s with mctx := mctx })

partial def collectLevelAux : Level → ClosureM Level
| u@(Level.succ v _)      => do v ← visitLevel collectLevelAux v; pure $ u.updateSucc! v
| u@(Level.max v w _)     => do v ← visitLevel collectLevelAux v; w ← visitLevel collectLevelAux w; pure $ u.updateMax! v w
| u@(Level.imax v w _)    => do v ← visitLevel collectLevelAux v; w ← visitLevel collectLevelAux w; pure $ u.updateIMax! v w
| u@(Level.mvar mvarId _) => mkNewLevelParam u
| u@(Level.param _ _)     => mkNewLevelParam u
| u@(Level.zero _)        => pure u

def collectLevel (u : Level) : ClosureM Level :=
visitLevel collectLevelAux u

def mkFreshFVarId : ClosureM FVarId := do
s ← get;
let id := s.ngen.curr;
modify $ fun s => { s with ngen := s.ngen.next };
pure id

/--
  Remark: This method does not guarantee unique user names.
  The correctness of the procedure does not rely on unique user names.
  Recall that the pretty printer takes care of unintended collisions. -/
def mkNextUserName : ClosureM Name := do
s ← get;
let n := (`_x).appendIndexAfter s.nextExprIdx;
modify $ fun s => { s with nextExprIdx := s.nextExprIdx + 1 };
pure n

def getUserName (userName? : Option Name) : ClosureM Name :=
match userName? with
| some userName => pure userName
| none          => mkNextUserName

def mkLocalDecl (userName? : Option Name) (type : Expr) (bi : BinderInfo) : ClosureM Expr := do
userName ← getUserName userName?;
fvarId   ← mkFreshFVarId;
modify $ fun s => { s with lctxOutput := s.lctxOutput.mkLocalDecl fvarId userName type bi };
pure $ mkFVar fvarId

def mkLetDecl (userName : Name) (type : Expr) (value : Expr) : ClosureM Expr := do
fvarId   ← mkFreshFVarId;
modify $ fun s => { s with lctxOutput := s.lctxOutput.mkLetDecl fvarId userName type value };
pure $ mkFVar fvarId

@[inline] def visitExpr (f : Expr → ClosureM Expr) (e : Expr) : ClosureM Expr :=
if !e.hasLevelParam && !e.hasFVar && !e.hasMVar then pure e
else do
  s ← get;
  match s.visitedExpr.find? e with
  | some r => pure r
  | none   => do
    r ← f e;
    modify $ fun s => { s with visitedExpr := s.visitedExpr.insert e r };
    pure r

partial def collectExprAux : Expr → ClosureM Expr
| e =>
  let collect (e : Expr) := visitExpr collectExprAux e;
  match e with
  | Expr.proj _ _ s _    => do s ← collect s; pure (e.updateProj! s)
  | Expr.forallE _ d b _ => do d ← collect d; b ← collect b; pure (e.updateForallE! d b)
  | Expr.lam _ d b _     => do d ← collect d; b ← collect b; pure (e.updateLambdaE! d b)
  | Expr.letE _ t v b _  => do t ← collect t; v ← collect v; b ← collect b; pure (e.updateLet! t v b)
  | Expr.app f a _       => do f ← collect f; a ← collect a; pure (e.updateApp! f a)
  | Expr.mdata _ b _     => do b ← collect b; pure (e.updateMData! b)
  | Expr.sort u _        => do u ← collectLevel u; pure (e.updateSort! u)
  | Expr.const c us _    => do us ← us.mapM collectLevel; pure (e.updateConst! us)
  | Expr.mvar mvarId _   => do
    mctx ← getMCtx;
    match mctx.findDecl? mvarId with
    | none          => throw "unknown metavariable"
    | some mvarDecl => do
      type ← instantiateMVars mvarDecl.type;
      type ← collect type;
      x    ← mkLocalDecl none type BinderInfo.default;
      modify $ fun s => { s with exprClosure := s.exprClosure.push e };
      pure x
  | Expr.fvar fvarId _ => do
    ctx ← read;
    match ctx.lctxInput.find? fvarId with
    | none => throw "unknown free variable"
    | some (LocalDecl.cdecl _ _ userName type bi) => do
      type ← instantiateMVars type;
      type ← collect type;
      x    ← mkLocalDecl userName type bi;
      modify $ fun s => { s with exprClosure := s.exprClosure.push e };
      pure x
    | some (LocalDecl.ldecl _ _ userName type value) =>
      if ctx.zeta then do
        value ← instantiateMVars value;
        collect value
      else do
        type  ← instantiateMVars type;
        type  ← collect type;
        value ← instantiateMVars value;
        value ← collect value;
        -- Note that let-declarations do not need to be provided to the closure being constructed.
        mkLetDecl userName type value
  | e => pure e

def collectExpr (e : Expr) : ClosureM Expr := do
e ← instantiateMVars e;
visitExpr collectExprAux e

structure MkClosureResult :=
(levelParams  : Array Name)
(type         : Expr)
(value        : Expr)
(levelClosure : Array Level)
(exprClosure  : Array Expr)
(mctx         : MetavarContext)

def mkClosure (mctx : MetavarContext) (lctx : LocalContext) (type : Expr) (value : Expr) (zeta : Bool := false) : Except String MkClosureResult :=
let shareCommonTypeValue : Std.ShareCommonM (Expr × Expr) := do {
  type  ← Std.withShareCommon type;
  value ← Std.withShareCommon value;
  pure (type, value)
};
let (type, value) := shareCommonTypeValue.run;
let mkTypeValue : ClosureM (Expr × Expr × MetavarContext) := do {
  type  ← collectExpr type;
  value ← collectExpr value;
  mctx  ← getMCtx;
  pure (type, value, mctx)
};
match (mkTypeValue { lctxInput := lctx, zeta := zeta }).run { mctx := mctx } with
| EStateM.Result.ok (type, value, mctx) s =>
  let fvars := s.lctxOutput.getFVars;
  let type  := s.lctxOutput.mkForall fvars type;
  let value := s.lctxOutput.mkLambda fvars value;
  Except.ok {
    levelParams  := s.levelParams,
    type         := type,
    value        := value,
    levelClosure := s.levelClosure,
    exprClosure  := s.exprClosure,
    mctx         := mctx }
| EStateM.Result.error ex s => Except.error ex

end Closure

def mkAuxDefinition (env : Environment) (opts : Options) (mctx : MetavarContext) (lctx : LocalContext) (name : Name) (type : Expr) (value : Expr)
    (zeta : Bool := false) : Except KernelException (Expr × Environment × MetavarContext) :=
match Closure.mkClosure mctx lctx type value zeta with
| Except.error ex  => throw $ KernelException.other ex
| Except.ok result => do
  let decl := Declaration.defnDecl {
    name     := name,
    lparams  := result.levelParams.toList,
    type     := result.type,
    value    := result.value,
    hints    := ReducibilityHints.regular (getMaxHeight env result.value + 1),
    isUnsafe := env.hasUnsafe result.type || env.hasUnsafe result.value
  };
  env ← env.addAndCompile opts decl;
  let c := mkAppN (mkConst name result.levelClosure.toList) result.exprClosure;
  pure (c, env, result.mctx)

end Lean
