/-
Copyright (c) 2022 Newell Jensen. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Newell Jensen, Thomas Murrills, Joachim Breitner
-/
prelude
import Lean.Meta.Tactic.Apply
import Lean.Elab.Tactic.Basic
import Lean.Meta.Tactic.Refl

/-!
# `rfl` tactic extension for reflexive relations

This extends the `rfl` tactic so that it works on any reflexive relation,
provided the reflexivity lemma has been marked as `@[refl]`.
-/

namespace Lean.Meta.Rfl

open Lean Meta

/-- Environment extensions for `refl` lemmas -/
initialize reflExt :
    SimpleScopedEnvExtension (Name × Array DiscrTree.Key) (DiscrTree Name) ←
  registerSimpleScopedEnvExtension {
    addEntry := fun dt (n, ks) => dt.insertCore ks n
    initial := {}
  }

initialize registerBuiltinAttribute {
  name := `refl
  descr := "reflexivity relation"
  add := fun decl _ kind => MetaM.run' do
    let declTy := (← getConstInfo decl).type
    let (_, _, targetTy) ← withReducible <| forallMetaTelescopeReducing declTy
    let fail := throwError
      "@[refl] attribute only applies to lemmas proving x ∼ x, got {declTy}"
    let .app (.app rel lhs) rhs := targetTy | fail
    if let .app (.const ``Eq [_]) _ := rel then
      throwError "@[refl] attribute may not be used on `Eq.refl`."
    unless ← withNewMCtxDepth <| isDefEq lhs rhs do fail
    let key ← DiscrTree.mkPath rel
    reflExt.add (decl, key) kind
}

open Elab Tactic

/-- `MetaM` version of the `rfl` tactic.

This tactic applies to a goal whose target has the form `x ~ x`, where `~` is a reflexive
relation, that is, equality or another relation which has a reflexive lemma tagged with the
attribute [refl].
-/
def _root_.Lean.MVarId.applyRfl (goal : MVarId) : MetaM Unit := goal.withContext do
  -- NB: uses whnfR, we do not want to unfold the relation itself
  let t ← whnfR <|← instantiateMVars <|← goal.getType
  if t.getAppNumArgs < 2 then
    throwTacticEx `rfl goal "expected goal to be a binary relation"

  -- Special case HEq here as it has a different argument order.
  if t.isAppOfArity ``HEq 4 then
    let gs ← goal.applyConst ``HEq.refl
    unless gs.isEmpty do
      throwTacticEx `rfl goal <| MessageData.tagged `Tactic.unsolvedGoals <| m!"unsolved goals\n{
        goalsToMessageData gs}"
    return

  let rel := t.appFn!.appFn!
  let lhs := t.appFn!.appArg!
  let rhs := t.appArg!

  let success ← approxDefEq <| isDefEqGuarded lhs rhs
  unless success do
    let explanation := MessageData.ofLazyM (es := #[lhs, rhs]) do
      let (lhs, rhs) ← addPPExplicitToExposeDiff lhs rhs
      return m!"the left-hand side{indentExpr lhs}\nis not definitionally equal to the right-hand side{indentExpr rhs}"
    throwTacticEx `rfl goal explanation

  if rel.isAppOfArity `Eq 1 then
    -- The common case is equality: just use `Eq.refl`
    let us := rel.appFn!.constLevels!
    let α :=  rel.appArg!
    goal.assign (mkApp2 (mkConst ``Eq.refl us) α lhs)
  else
    -- Else search through `@refl` keyed by the relation
    -- We change the type to `lhs ~ lhs` so that we do not the (possibly costly) `lhs =?= rhs` check
    -- again.
    goal.setType (.app t.appFn! lhs)
    let s ← saveState
    let mut ex? := none
    for lem in ← (reflExt.getState (← getEnv)).getMatch rel do
      try
        let gs ← goal.apply (← mkConstWithFreshMVarLevels lem)
        if gs.isEmpty then return () else
          throwError MessageData.tagged `Tactic.unsolvedGoals <| m!"unsolved goals\n{
            goalsToMessageData gs}"
      catch e =>
        unless ex?.isSome do
          ex? := some (← saveState, e) -- stash the first failure of `apply`
      s.restore
    if let some (sErr, e) := ex? then
      sErr.restore
      throw e
    else
      throwTacticEx `rfl goal m!"no @[refl] lemma registered for relation{indentExpr rel}"

/-- Helper theorem for `Lean.MVarId.liftReflToEq`. -/
private theorem rel_of_eq_and_refl {α : Sort _} {R : α → α → Prop}
    {x y : α} (hxy : x = y) (h : R x x) : R x y :=
  hxy ▸ h

/--
Convert a goal of the form `x ~ y` into the form `x = y`, where `~` is a reflexive
relation, that is, a relation which has a reflexive lemma tagged with the attribute `@[refl]`.
If this can't be done, returns the original `MVarId`.
-/
def _root_.Lean.MVarId.liftReflToEq (mvarId : MVarId) : MetaM MVarId := do
  mvarId.checkNotAssigned `liftReflToEq
  let .app (.app rel _) _ ← withReducible mvarId.getType' | return mvarId
  if rel.isAppOf `Eq then
    -- No need to lift Eq to Eq
    return mvarId
  for lem in ← (reflExt.getState (← getEnv)).getMatch rel do
    let res ← observing? do
      -- First create an equality relating the LHS and RHS
      -- and reduce the goal to proving that LHS is related to LHS.
      let [mvarIdEq, mvarIdR] ← mvarId.apply (← mkConstWithFreshMVarLevels ``rel_of_eq_and_refl)
        | failure
      -- Then fill in the proof of the latter by reflexivity.
      let [] ← mvarIdR.apply (← mkConstWithFreshMVarLevels lem) | failure
      return mvarIdEq
    if let some mvarIdEq := res then
      return mvarIdEq
  return mvarId

end Lean.Meta.Rfl
