/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Joachim Breitner
-/

module
prelude
public import Lean.Meta.Tactic.Simp.SimpTheorems
import Lean.Meta.Tactic.Simp.Main

namespace Lean

open Meta

structure MethodSpecTheorem where
  /-- Name of the implementation function -/
  name : Name
  levelParams : List Name
  /-- `opImpl = Cls.op instClsT` -/
  type : Expr

structure MethodSpecsInfo where
  clsName : Name
  /-- Whether the specs should be public or private -/
  privateSpecs : Bool
  /-- Array mapping field names to implementation functions. -/
  fieldImpls : Array (Name × Name)
  /-- rewrite rules to apply -/
  thms  : Array MethodSpecTheorem

/--
This function checks the `instName` for eligibility and collects the information to rewrite.
It is run twice: when setting the `@[specs]` attribute as a preflight check, and when actually realizing
the constants.
-/
def getMethodSpecsInfo (instName : Name) : MetaM MethodSpecsInfo := do
  let instInfo ← getConstInfoDefn instName
  let some clsName ← isClass? instInfo.type
    | throwError "expected `{.ofConstName instName}` to be a type class instance, but its \
         type{inlineExpr instInfo.type}does not look like a class."
  let instArity ← forallTelescopeReducing instInfo.type fun xs _ => pure xs.size
  let some structInfo := getStructureInfo? (← getEnv) clsName
    | throwError "`{.ofConstName clsName}` is not a structure"

  lambdaTelescope instInfo.value fun xs body => do
    let inst := mkAppN (mkConst instInfo.name (instInfo.levelParams.map mkLevelParam)) xs
    let clsApp ← instantiateForall instInfo.type xs
    unless xs.size == instArity && (← isConstructorApp body) do
      throwError "the definition of `{.ofConstName instName}` does not have the expected shape"

    let mut fieldImpls := #[]
    let mut thms := #[]
    let mut privateSpecs := false

    for field in structInfo.fieldNames, arg in body.getAppArgsN structInfo.fieldNames.size do
      if (← isProof arg) then continue
      let arg := arg.eta
      let f := arg.getAppFn
      let ys := arg.getAppArgs
      unless f.isConst do
        throwError "field `{field}` of the instance is not an application of a constant"
      unless f.constLevels! == instInfo.levelParams.map mkLevelParam do
        throwError "function `{f}` is called with universe parameters\n  {f.constLevels!}\nwhich differs from \
          the instances' universe parameters\n  {instInfo.levelParams.map mkLevelParam}"
      unless xs == ys do
        throwError "function `{f}` does not take its arguments in the same order as the instance"
      let implName := f.constName!
      let isExposed := !(← getEnv).header.isModule || (((← getEnv).setExporting true).find? implName).elim false (·.hasValue)
      unless isExposed do
        privateSpecs := true
      -- Construct the replacement theorems
      let some fieldInfo := getFieldInfo? (← getEnv) clsName field
        | throwError "internal error: could not find field {field} in structure {clsName}"
      let lhs := arg
      let projFn := mkConst fieldInfo.projFn clsApp.getAppFn.constLevels!
      let rhs := mkAppN projFn (clsApp.getAppArgs ++ #[inst])
      let eq ← mkEq lhs rhs
      let thm ← mkForallFVars xs eq
      unless (← isDefEq lhs rhs) do
        throwError "internal error: equation `{eq}` does not hold definitionally"
      fieldImpls := fieldImpls.push (field, implName)
      thms := thms.push { name := implName, levelParams := instInfo.levelParams, type := thm }
    trace[Meta.MethodSpecs] "MethodSpecs for {instName}:\n{fieldImpls}\n\
      thms: {thms.map (·.type)}\nprivateSpecs: {privateSpecs}"

    return {clsName, fieldImpls, thms, privateSpecs}

public structure MethodSpecsAttrData where
  clsName : Name
  /-- Whether the specs should be public or private -/
  privateSpecs : Bool
deriving Inhabited

def getParam (instName : Name) (_stx : Syntax) : AttrM MethodSpecsAttrData := do
  -- Preflight check
  let specsInfo ← (getMethodSpecsInfo instName).run'
  return { specsInfo with }

/--
Generate method specification theorems for the methods of the given type class instance.

This expects all (non-proof) methods of the instance to be defined via separate helper functions,
which must take the same arguments as the instance itself, in the same order.

If it is applied to an instance
```
instance instClsT : Cls T where op := opImpl
```
it produces a theorem `instClsT.op_spec` based on `opImpl.eq_def`, but phrased in terms of the
overloaded `Cls.op` operation, and similarly `instClsT.op_spec_<n>` based on the equational theorems
`opImpl.eq_<n>`.
-/
@[builtin_doc]
builtin_initialize methodSpecsAttr : ParametricAttribute MethodSpecsAttrData ←
  registerParametricAttribute {
    name := `method_specs
    descr := "generate method specification theorems"
    getParam
  }

builtin_initialize methodSpecsSimpExtension : SimpExtension ←
  registerSimpAttr `method_specs_simp
    "simp lemma used to post-process the theorem created by `@[method_specs]`."

def mkSpecTheoremName (env : Environment) (instName : Name) (privateSpecs : Bool) (suffix : String) : Name :=
  let thmName := instName.str suffix
  if privateSpecs then mkPrivateName env thmName else thmName

def startsWithFollowedByNumber (s p : String) : Bool :=
  s.startsWith p && (s.drop p.length).isNat

def isSpecThmLikeSuffix (fieldName : Name) (s : String) : Bool :=
  s == s!"{fieldName}_spec" || startsWithFollowedByNumber s s!"{fieldName}_spec_"

/--
The spec theorem theorem for an instance can be private even if the instance itself is not.
So un-private the name here when looking for a declaration, and finally check if it matches.
Cf. `Lean.Meta.declFromEqLikeName`. Maybe worth collecting this logic in a central place.
-/
def isSpecThmNameFor (env : Environment) (name : Name) : Option Name := do
  let .str p s := name | none
  [p, privateToUserName p].firstM fun p => do
      let attrData ← methodSpecsAttr.getParam? env p
      for fieldName in getStructureFields env attrData.clsName do
        if isSpecThmLikeSuffix fieldName s then
          if name == mkSpecTheoremName env p attrData.privateSpecs s then
            return p
      none

def rewriteThm (ctx : Simp.Context) (simprocs : Simprocs)
    (eqThmName destThmName : Name) : MetaM Unit := do
  let thmInfo ← getConstVal eqThmName
  let (result, _) ← simp thmInfo.type ctx (simprocs := #[simprocs])
  trace[Meta.MethodSpecs] "type for {destThmName}:{indentExpr result.expr}"
  let eqThmApp := mkConst eqThmName (thmInfo.levelParams.map mkLevelParam)
  let value := mkAppN (mkConst ``Eq.mp [0]) #[thmInfo.type, result.expr, ← result.getProof, eqThmApp]
  addDecl <| Declaration.thmDecl {
    name          := destThmName
    levelParams   := thmInfo.levelParams
    type          := result.expr
    value         := value
  }

def genSpecs (instName : Name) : MetaM Unit := do
  let methodSpecsInfo ← getMethodSpecsInfo instName
  let key := mkSpecTheoremName (← getEnv) instName methodSpecsInfo.privateSpecs s!"{methodSpecsInfo.fieldImpls[0]!.1}_spec"
  realizeConst instName key doRealize
where
  doRealize := do
    let methodSpecsInfo ← getMethodSpecsInfo instName
    withoutExporting (when := methodSpecsInfo.privateSpecs) do
      let mut s ← methodSpecsSimpExtension.getTheorems
      for thm in methodSpecsInfo.thms do
        trace[Meta.MethodSpecs] "adding simp theorem for {thm.name} : {thm.type}"
        s := s.addSimpTheorem <| ← mkDSimpTheorem (.other thm.name) thm.levelParams.toArray thm.type
      let ctx ← Simp.mkContext
        (simpTheorems  := #[s])
        (congrTheorems := (← getSimpCongrTheorems))
        (config        := { } )
      let simprocs ← Simp.getSimprocs

      let env ← getEnv
      for (fieldName, implName) in methodSpecsInfo.fieldImpls do
        let some unfoldThm ← getUnfoldEqnFor? implName (nonRec := true)
          | throwError "failed to generate unfolding theorem for {.ofConstName implName}"
        let thmName := mkSpecTheoremName env instName methodSpecsInfo.privateSpecs s!"{fieldName}_spec"
        rewriteThm ctx simprocs unfoldThm thmName

        if let some eqnThms ← getEqnsFor? implName then
          for eqnThm in eqnThms, i in [:eqnThms.size] do
            let thmName := mkSpecTheoremName env instName methodSpecsInfo.privateSpecs s!"{fieldName}_spec_{i+1}"
            rewriteThm ctx simprocs eqnThm thmName


public partial def getMethodSpecTheorem (instName : Name) (op : String) : MetaM (Option Name) := do
  let env ← getEnv
  let some methodSpecInfos := methodSpecsAttr.getParam? env instName | return none
  let thmName := mkSpecTheoremName env instName methodSpecInfos.privateSpecs s!"{op}_spec"
  realizeGlobalConstNoOverloadCore thmName

public partial def getMethodSpecTheorems (instName : Name) (op : String) : MetaM (Option (Array Name)) := do
  let some methodSpecInfos := methodSpecsAttr.getParam? (← getEnv) instName | return none
  -- Realize spec theorems
  let thmName := mkSpecTheoremName (← getEnv) instName methodSpecInfos.privateSpecs s!"{op}_spec"
  let _ ← realizeGlobalConstNoOverloadCore thmName
  -- Now collect the generated ones
  let mut i := 0
  let mut thms := #[]
  let env ← getEnv
  while true do
    let thmName := mkSpecTheoremName (← getEnv) instName methodSpecInfos.privateSpecs s!"{op}_spec_{i+1}"
    if env.containsOnBranch thmName then
      thms := thms.push thmName
      i := i + 1
    else
      break
  return some thms

builtin_initialize
  registerReservedNamePredicate fun env name => isSpecThmNameFor env name |>.isSome

  registerReservedNameAction fun name => do
    if let some instName := isSpecThmNameFor (← getEnv) name then
      (genSpecs instName).run'
      return true
    return false


builtin_initialize
   Lean.registerTraceClass `Meta.MethodSpecs
