/-
Copyright (c) 2019 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
import Lean.Meta.Basic

namespace Lean
namespace Meta

private partial def decAux? : Level → MetaM (Option Level)
| Level.zero _        => pure none
| Level.param _ _     => pure none
| Level.mvar mvarId _ => do
  mctx ← getMCtx;
  match mctx.getLevelAssignment? mvarId with
  | some u => decAux? u
  | none   =>
    condM (isReadOnlyLevelMVar mvarId) (pure none) $ do
      u ← mkFreshLevelMVar;
      assignLevelMVar mvarId (mkLevelSucc u);
      pure u
| Level.succ u _  => pure u
| u =>
  let process (u v : Level) : MetaM (Option Level) := do {
    u? ← decAux? u;
    match u? with
    | none   => pure none
    | some u => do
      v? ← decAux? v;
      match v? with
      | none   => pure none
      | some v => pure $ mkLevelMax u v
  };
  match u with
  | Level.max u v _  => process u v
  /- Remark: If `decAux? v` returns `some ...`, then `imax u v` is equivalent to `max u v`. -/
  | Level.imax u v _ => process u v
  | _                => unreachable!

partial def decLevel? (u : Level) : MetaM (Option Level) := do
mctx ← getMCtx;
result? ← decAux? u;
match result? with
| some v => pure $ some v
| none   => do
  modify $ fun s => { s with mctx := mctx };
  pure none

private def strictOccursMaxAux (lvl : Level) : Level → Bool
| Level.max u v _ => strictOccursMaxAux u || strictOccursMaxAux v
| u               => u != lvl && lvl.occurs u

/--
  Return true iff `lvl` occurs in `max u_1 ... u_n` and `lvl != u_i` for all `i in [1, n]`.
  That is, `lvl` is a proper level subterm of some `u_i`. -/
private def strictOccursMax (lvl : Level) : Level → Bool
| Level.max u v _ => strictOccursMaxAux lvl u || strictOccursMaxAux lvl v
| _               => false

/-- `mkMaxArgsDiff mvarId (max u_1 ... (mvar mvarId) ... u_n) v` => `max v u_1 ... u_n` -/
private def mkMaxArgsDiff (mvarId : MVarId) : Level → Level → Level
| Level.max u v _,     acc => mkMaxArgsDiff v $ mkMaxArgsDiff u acc
| l@(Level.mvar id _), acc => if id != mvarId then mkLevelMax acc l else acc
| l,                   acc => mkLevelMax acc l

/--
  Solve `?m =?= max ?m v` by creating a fresh metavariable `?n`
  and assigning `?m := max ?n v` -/
private def solveSelfMax (mvarId : MVarId) (v : Level) : MetaM Unit := do
n ← mkFreshLevelMVar;
assignLevelMVar mvarId $ mkMaxArgsDiff mvarId v n

private def postponeIsLevelDefEq (lhs : Level) (rhs : Level) : MetaM Unit :=
modify $ fun s => { s with postponed := s.postponed.push { lhs := lhs, rhs := rhs } }

inductive LevelConstraintKind
| mvarEq         -- ?m =?= l         where ?m does not occur in l
| mvarEqSelfMax  -- ?m =?= max ?m l  where ?m does not occur in l
| other

private def getLevelConstraintKind (u v : Level) : MetaM LevelConstraintKind :=
match u with
| Level.mvar mvarId _ =>
  condM (isReadOnlyLevelMVar mvarId)
    (pure LevelConstraintKind.other)
    (if !u.occurs v then pure LevelConstraintKind.mvarEq
     else if !strictOccursMax u v then pure LevelConstraintKind.mvarEqSelfMax
     else pure LevelConstraintKind.other)
| _ =>
  pure LevelConstraintKind.other

partial def isLevelDefEqAux : Level → Level → MetaM Bool
| Level.succ lhs _, Level.succ rhs _ => isLevelDefEqAux lhs rhs
| lhs, rhs =>
  if lhs == rhs then
    pure true
  else do
    trace! `Meta.isLevelDefEq.step (lhs ++ " =?= " ++ rhs);
    lhs' ← instantiateLevelMVars lhs;
    let lhs' := lhs'.normalize;
    rhs' ← instantiateLevelMVars rhs;
    let rhs' := rhs'.normalize;
    if lhs != lhs' || rhs != rhs' then
      isLevelDefEqAux lhs' rhs'
    else do
      mctx ← getMCtx;
      if !mctx.hasAssignableLevelMVar lhs && !mctx.hasAssignableLevelMVar rhs then do
        ctx ← read;
        if ctx.config.isDefEqStuckEx && (lhs.isMVar || rhs.isMVar) then do
          trace! `Meta.isLevelDefEq.stuck (lhs ++ " =?= " ++ rhs);
          throwEx $ Exception.isLevelDefEqStuck lhs rhs
        else
          pure false
      else do
        k ← getLevelConstraintKind lhs rhs;
        match k with
        | LevelConstraintKind.mvarEq        => do assignLevelMVar lhs.mvarId! rhs; pure true
        | LevelConstraintKind.mvarEqSelfMax => do solveSelfMax lhs.mvarId! rhs; pure true
        | _ => do
          k ← getLevelConstraintKind rhs lhs;
          match k with
          | LevelConstraintKind.mvarEq        => do assignLevelMVar rhs.mvarId! lhs; pure true
          | LevelConstraintKind.mvarEqSelfMax => do solveSelfMax rhs.mvarId! lhs; pure true
          | _ =>
            if lhs.isMVar || rhs.isMVar then
              pure false
            else
              let isSuccEq (u v : Level) : MetaM Bool :=
                match u with
                | Level.succ u _ => do
                  v? ← decLevel? v;
                  match v? with
                  | some v => isLevelDefEqAux u v
                  | none   => pure false
                | _ => pure false;
              condM (isSuccEq lhs rhs) (pure true) $
              condM (isSuccEq rhs lhs) (pure true) $ do
              postponeIsLevelDefEq lhs rhs; pure true

def isListLevelDefEqAux : List Level → List Level → MetaM Bool
| [],    []    => pure true
| u::us, v::vs => isLevelDefEqAux u v <&&> isListLevelDefEqAux us vs
| _,     _     => pure false

private def getNumPostponed : MetaM Nat := do
s ← get; pure s.postponed.size

open Std (PersistentArray)

private def getResetPostponed : MetaM (PersistentArray PostponedEntry) := do
s ← get;
let ps := s.postponed;
modify $ fun s => { s with postponed := {} };
pure ps

private def processPostponedStep : MetaM Bool :=
traceCtx `Meta.isLevelDefEq.postponed.step $ do
  ps ← getResetPostponed;
  ps.foldlM
    (fun (r : Bool) (p : PostponedEntry) =>
      if r then
        isLevelDefEqAux p.lhs p.rhs
      else
        pure false)
    true

private partial def processPostponedAux : Unit → MetaM Bool
| _ => do
  numPostponed ← getNumPostponed;
  if numPostponed == 0 then
    pure true
  else do
    trace! `Meta.isLevelDefEq.postponed ("processing #" ++ toString numPostponed ++ " postponed is-def-eq level constraints");
    r ← processPostponedStep;
    if !r then
      pure r
    else do
      numPostponed' ← getNumPostponed;
      if numPostponed' == 0 then
        pure true
      else if numPostponed' < numPostponed then
        processPostponedAux ()
      else do
        trace! `Meta.isLevelDefEq.postponed (format "no progress solving pending is-def-eq level constraints");
        pure false

private def processPostponed : MetaM Bool := do
numPostponed ← getNumPostponed;
if numPostponed == 0 then pure true
else traceCtx `Meta.isLevelDefEq.postponed $ processPostponedAux ()

def restore (env : Environment) (mctx : MetavarContext) (postponed : PersistentArray PostponedEntry) : MetaM Unit :=
modify $ fun s => { s with env := env, mctx := mctx, postponed := postponed }

/--
  `commitWhenSome? x` executes `x` and process all postponed universe level constraints produced by `x`.
  We keep the modifications only if `processPostponed` return true and `x` returned `some a`.

  Remark: postponed universe level constraints must be solved before returning. Otherwise,
  we don't know whether `x` really succeeded. -/
@[specialize] def commitWhenSome? {α} (x? : MetaM (Option α)) : MetaM (Option α) := do
s ← get;
let env       := s.env;
let mctx      := s.mctx;
let postponed := s.postponed;
modify $ fun s => { s with postponed := {} };
catch
  (do
    a? ← x?;
    match a? with
    | some a =>
      (condM processPostponed
        (pure (some a))
        (do restore env mctx postponed; pure none))
    | none => do
      restore env mctx postponed; pure none)
  (fun ex => do restore env mctx postponed; throw ex)

@[specialize] def commitWhen (x : MetaM Bool) : MetaM Bool := do
r? ← commitWhenSome? (condM x (pure $ some ()) (pure none));
match r? with
| some _ => pure true
| none   => pure false

/- Public interface -/

def isLevelDefEq (u v : Level) : MetaM Bool :=
traceCtx `Meta.isLevelDefEq $ do
  b ← commitWhen $ isLevelDefEqAux u v;
  trace! `Meta.isLevelDefEq (u ++ " =?= " ++ v ++ " ... " ++ if b then "success" else "failure");
  pure b

def isExprDefEq (t s : Expr) : MetaM Bool :=
traceCtx `Meta.isDefEq $ do
  b ← commitWhen $ isExprDefEqAux t s;
  trace! `Meta.isDefEq (t ++ " =?= " ++ s ++ " ... " ++ if b then "success" else "failure");
  pure b

abbrev isDefEq := @isExprDefEq

@[init] private def regTraceClasses : IO Unit := do
registerTraceClass `Meta.isLevelDefEq;
registerTraceClass `Meta.isLevelDefEq.step;
registerTraceClass `Meta.isLevelDefEq.postponed

end Meta
end Lean
