/-
Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
module
public import Lean.Meta
import Lean.Elab
import Lean.Meta.Sym.Simp.Theorems

open Lean Parser Meta Elab Tactic Sym

def timeItMs (k : MetaM α) : MetaM (α × UInt64) := do
  let startTime ← IO.monoNanosNow
  let a ← k
  let endTime ← IO.monoNanosNow
  let ms := (endTime - startTime).toFloat / 1000000.0
  return (a, ms.toUInt64)

/-- Helper function for executing a tactic `k` for solving `$(goal) n`. -/
def driver (goal : Name) (unfold : List Name) (n : Nat) (discharge : MetaM (TSyntax `tactic)) (k : MVarId → MetaM (List MVarId)) : MetaM Unit := do
  let mvar ← mkFreshExprMVar (mkApp (mkConst goal) (mkNatLit n))
  let (mvarId, _unfoldMs) ← timeItMs do SymM.run do
    let mvarId ← preprocessMVar mvar.mvarId!
    let eqnss ← unfold.toArray
      |>.push goal
      |>.mapM fun n => getEqnsFor? n
    let thms := eqnss.flatMap (fun o => o.getD #[])
    match (← Sym.simpGoal mvarId (← Sym.mkMethods thms)) with
    | .goal mvarId => return mvarId
    | .noProgress => throwError "No progress when simping {mvarId}!"
    | .closed => throwError "Simp closed goal {mvarId}"
  -- IO.println s!"time spent unfolding: {_unfoldMs} ms"
  let (mvarIds, ms) ← timeItMs do k mvarId
  let discharge ← discharge
  let dischargePp ← PrettyPrinter.ppTactic discharge
  let dischargeMs? ← OptionT.run <| do
    guard !mvarIds.isEmpty
    Prod.snd <$> timeItMs do
      for mvarId in mvarIds do
        let ([], _) ← Lean.Elab.runTactic mvarId discharge.raw {} {}
          | throwError "{dischargePp} failed to solve {mvarId}"
  let (expr, instMs) ← timeItMs (instantiateMVars mvar)
  -- Emulate the shareCommonPreDefs step before sending the term to the kernel.
  -- If we don't do this, kernel checking time balloons.
  let expr ← SymM.run (shareCommon expr)
  let (_, kernelMs) ← timeItMs (checkWithKernel expr)
  let label := s!"{goal.getPrefix}({n}):"
  let pad := "".pushn ' ' (24 - min label.length 24)
  let mut msg := s!"{label}{pad}{ms} ms"
  if let some dischargeMs := dischargeMs? then
    msg := msg ++ s!", {mvarIds.length} VCs by {dischargePp}: {dischargeMs} ms"
  else
    msg := msg ++ s!", {mvarIds.length} VCs"
  if instMs > 1000 then
    msg := msg ++ s!", instantiate > 1000ms: {instMs} ms"
  msg := msg ++ s!", kernel: {kernelMs} ms"
  IO.println msg

def solveUsingTactic (goal : Name) (unfold : List Name) (n : Nat) (solve : MetaM (TSyntax `tactic)) (discharge : MetaM (TSyntax `tactic)) : MetaM Unit := do
  driver goal unfold n discharge fun mvarId => do
    let (mvarIds, _) ← Lean.Elab.runTactic mvarId (← solve).raw {} {}
    return mvarIds

/--
Solves a goal of the form `goal n` using the given tactic, where `n` ranges over `sizes`.
`unfold` is a list of `simp` lemmas to apply in order to unfold `goal n`.
For many benchmarks, this is `[step, loop]`.
-/
public def runBenchUsingTactic (goal : Name) (unfold : List Name) (solve : MetaM (TSyntax `tactic)) (discharge : MetaM (TSyntax `tactic)) (sizes : List Nat) : MetaM Unit := do
  for n in sizes do
    resetCache
    solveUsingTactic goal unfold n solve discharge

def solveUsingSym (goal : Name) (unfold : List Name) (n : Nat) (solve : MVarId → SymM (List MVarId)) (discharge : MetaM (TSyntax `tactic)) : MetaM Unit := do
  driver goal unfold n discharge fun mvarId => SymM.run do solve mvarId

/--
Solves a goal of the form `goal n` using a `SymM` procedure, where `n` ranges over `sizes`.
`unfold` is a list of `simp` lemmas to apply in order to unfold `goal n`.
For many benchmarks, this is `[step, loop]`.
-/
public def runBenchUsingSym (goal : Name) (unfold : List Name) (solve : MVarId → SymM (List MVarId)) (discharge : MetaM (TSyntax `tactic)) (sizes : List Nat) : MetaM Unit := do
  for n in sizes do
    resetCache
    solveUsingSym goal unfold n solve discharge
