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

namespace Lean.Elab.Tactic
open Meta

private def equivMVarDecl (d₁ d₂ : MetavarDecl) : Bool :=
  d₁.type == d₂.type
  -- The following additional check does not seem to be necessary
/-
  &&
  d₁.lctx.decls.size == d₂.lctx.decls.size &&
  Id.run do
     for i in [:d₁.lctx.decls.size] do
       match d₁.lctx.decls[i], d₂.lctx.decls[i] with
       | some localDecl₁, some localDecl₂ => if localDecl₁.type != localDecl₂.type then return false
       | none, none => pure ()
       | _, _ => return false
     return true
-/

register_builtin_option tactic.dbg_cache : Bool := {
  defValue := false
  group    := "tactic"
  descr    := "enable tactic cache debug messages (remark: they are sent to the standard error)"
}

private def dbg_cache (msg : String) : TacticM Unit := do
  if tactic.dbg_cache.get (← getOptions) then
    dbg_trace msg

private def dbg_cache' (cacheRef : IO.Ref Cache) (pos : String.Pos) (mvarId : MVarId) (msg : String) : TacticM Unit := do
  if tactic.dbg_cache.get (← getOptions) then
    let {line, column} := (← getFileMap).toPosition pos
    dbg_trace "{msg}, cache size: {(← cacheRef.get).pre.size}, line: {line}, column: {column}, contains entry: {(← cacheRef.get).pre.find? { mvarId, pos } |>.isSome}"

private def findCache? (cacheRef : IO.Ref Cache) (mvarId : MVarId) (stx : Syntax) (pos : String.Pos) : TacticM (Option Snapshot) := do
  let some s := (← cacheRef.get).pre.find? { mvarId, pos } | do dbg_cache "cache key not found"; return none
  let mvarDecl ← getMVarDecl mvarId
  let some mvarDeclOld := s.meta.mctx.findDecl? mvarId | return none
  if equivMVarDecl mvarDecl mvarDeclOld then
    if stx == s.stx then
      return some s
    else
      dbg_cache "syntax is different"
      return none
  else
    dbg_cache "cached state is not compatible"
    return none

@[builtinTactic checkpoint] def evalCheckpoint : Tactic := fun stx =>
  focus do
    let mvarId ← getMainGoal
    let some cacheRef := (← readThe Term.Context).tacticCache? | evalTactic stx[1]
    let some pos := stx.getPos? | evalTactic stx[1]
    match (← findCache? cacheRef mvarId stx[1] pos) with
    | some s =>
      cacheRef.modify fun { pre, post } => { pre, post := post.insert { mvarId, pos } s }
      set s.core
      set s.meta
      set s.term
      set s.tactic
      dbg_cache' cacheRef pos mvarId "cache hit"
    | none =>
      evalTactic stx[1]
      let s := {
        stx    := stx[1]
        core   := (← getThe Core.State)
        meta   := (← getThe Meta.State)
        term   := (← getThe Term.State)
        tactic := (← get)
      }
      dbg_cache' cacheRef pos mvarId "cache miss"
      cacheRef.modify fun { pre, post } => { pre, post := post.insert { mvarId, pos } s }

end Lean.Elab.Tactic
