import Batteries
import Lean
import Qq

/-!
# Tutorial: Writing `rw` from scratch

Content
(1) What `rw` does on proof term level?
(2) Basic implementation
(3) Options for normalization
(4) Unification - rewriting a quantified equality
-/

open Lean Elab Meta Tactic
open Qq

/-
## (1) What `rw` does on proof term level

Let us look at an example proof using `rw`.
-/

theorem rw_example (a b : Nat) (f : Nat → Nat) (p : Nat → Prop)
    (h_eq  : ∀ x : Nat, f x = x) (h_finish : p (a + b + a + b)) :
    p (f a + f b + f a + f b) := by
  rw [h_eq] -- rewrites two instances of `f a`
  rw [h_eq] -- rewrites two instances of `f b`
  exact h_finish

-- and print its proof term
#print rw_example

-- it should look similar to
example : ∀ (a b : Nat) (f : Nat → Nat) (p : Nat → Prop),
  (∀ (x : Nat), f x = x) → p (a + b + a + b) → p (f a + f b + f a + f b) :=
fun a b f p h_eq h_finish =>
  Eq.mpr (
    congrArg (fun X => p (X + f b + X + f b)) (h_eq a : f a = a)
    : p (f a + f b + f a + f b) = p (a + f b + a + f b)
  )
  (Eq.mpr (
    congrArg (fun X => p (a + X + a + X)) (h_eq b : f b = b)
    : p (a + f b + a + f b) = p (a + b + a + b)
  ) h_finish)

-- You can see two main theorems used for each rewrite:
#check congrArg -- digging into a subterm
#check Eq.mpr -- translate equality to implication

/-
Because of the theorems that `rw` is using, it first instantiates
the hypothesis `h_eq`, and then replaces all the instances in the goal
by a single call of `congrArg`.
-/

/-
(intermezzo) the role of `id`
You can also see some `id` in the proof which is the identity function.
It is used for typing -- `id` can change a type of its value in case
they are definitionally equal.
On metaprogramming level, this appeared in the proof by using
-/
#check mkExpectedPropHint

/-
## (2) Implementing basic `rw`

### Abstracting a variable

In the basic version, we will be only rewriting a term
with a single equality without quantifiers.

Say we are rewriting `A = B` in some expression
`e := ... A ... A ...`
First, we want to locate all the `a`-s in the expression,
and build a mapping
`fun X => ... X ... X ...`

This is called an abstraction, first we need to locate all `A`s in `e`,
and replace them with a corresponding `bvar`. A `bvar` is a variable
pointing to a quantifier with an index where quantifiers are indexed
from the inner-most to the outer-most.
To determine which `bvar` index to use, we carry `offset`.
-/
/-- (tutorial function) simplified `kabstract` -/
def myAbstract (e a : Expr) (offset : Nat := 0) : MetaM Expr := do
  if !e.hasLooseBVars then  -- we cannot rewrite a subterm containing a variable bound in `e`
    if (← isDefEq e a) then  -- check if already `a` is already the root
      return mkBVar offset  -- replace with bvar
  -- otherwise, we recurse
  match e with -- ctrl-click on `e.update...` to see the definition
  | .app f x         => return e.updateApp! (← myAbstract f a offset) (← myAbstract x a offset)
  | .mdata _ b       => return e.updateMData! (← myAbstract b a offset) -- `b` = body
  | .proj _ _ b      => return e.updateProj! (← myAbstract b a offset)
  | .letE _ t v b _  =>
    return e.updateLetE! (← myAbstract t a offset) -- diving under a binder -> increase offset
      (← myAbstract v a offset) (← myAbstract b a (offset+1))
  | .lam _ d b _     => return e.updateLambdaE! (← myAbstract d a offset) (← myAbstract b a (offset+1))
  | .forallE _ d b _ => return e.updateForallE! (← myAbstract d a offset) (← myAbstract b a (offset+1))
  | e                => return e

-- lean's implementation is slightly more advanced but still readable
#check kabstract

-- the difference is that `kabstract` allows choosing positions,
-- where to apply the rewrite
def myAbstractAt (pos : Nat) (e a : Expr) :=
  kabstract e a (.pos [pos])

-- Let's see
example (a b : Nat) (h : (2 * a + b + 1) = (2 * b + a + 1)) : True := by
  run_tacq
    let e1 ← myAbstract h.ty a
    let e2 ← kabstract h.ty a
    -- kabstract does the same as myAbstract
    -- the #0 represents a bvar which is not bounded in the printed term
    logInfo m!"e1: {e1}\ne2: {e2}"
    -- Let's see how we can choose the positions for `kabstract`.
    logInfo m!"pos1: {← myAbstractAt 1 h.ty a}"
    logInfo m!"pos2: {← myAbstractAt 2 h.ty a}"
    -- exercise: Re-implement `myAbstractAt` from scratch,
    -- without using `kabstract`. Can you understand how
    -- `kabstract` uses a State monad to do it?
  trivial

-- Now, we can build the mapping
/-- (tutorial function) Runs `kabstract`, and wraps the result to a lambda. -/
def abstractToMapping (e a : Expr) : MetaM Expr := do
  -- make sure that all previous mvar assignments were applied in `e`
  -- this is necessary to recurse the term, not for `isDefEq`, so `a` doesn't need it
  let e ← instantiateMVars e
  let body ← myAbstract e a  -- replace `a` with a `bvar`
  return mkLambda
    `X -- name for the variable, due to `bvar`s, we don't have to care about collisions,
    BinderInfo.default -- could be also e.g. `implicit` (braces instead of parentheses)
    (← inferType a) -- type for the variable
    body

-- Let's build the lambda
example (A : Nat) (h : -- some crazy expression containing various constructions
    ∀ (y : Nat), (y = A) →
    let z := A + y
    y + A = (fun (X : Nat) => X + A - y) z -- no worry about the same name
  ) : True := by
  -- `run_tacq` is like `run_tac` but makes the context directly accessible
  -- not useful for writing tactics but handy for testing
  run_tacq
    logInfo m!"Before: {h.ty}" -- Qq-type (compile-time)
    let f ← abstractToMapping h.ty A
    logInfo m!"After: {f}"
  trivial

/-
### Decomposing equality
-/
/-- (tutorial function)
`decomposeEq` takes a proof of equality `pf : a = b`, where `(a b : α : Sort u)`,
and returns (u, α, a b)
-/
def decomposeEq (pf : Expr) : MetaM (Level × Expr × Expr × Expr) := do
  let t ← inferType pf
  -- `whnf` helps to get `Eq` at the root, removing mdata,
  -- instantiating mvars, expanding definitions...
  let t ← whnf t
  match t with
  | .app (.app (.app (.const ``Eq [u]) α) a) b =>
    return (u, α, a, b)
  | _ => throwError "given term {pf} : {t} is not a proof of equality"

-- we also could have used `Expr.app3? ``Eq`, or
-- `matchEq?` but that doesn't give us the level
#check Expr.app3?

/-
### Building proof term
-/

/--
(tutorial function)
Takes a term `t := ... A ...`, and an equation
`eq : A = B`, and returns a proof of `... B ... → ... A ...`.
-/
def proveRwImp (eq t : Expr) :
  MetaM Expr := do
  let (u, α, a, b) ← decomposeEq eq
  -- find the abstract function
  let f ← abstractToMapping t a
  logInfo m!"lhs := {a}, rhs := {b}\nabstr := {f}"
  -- we also detect the sort level of t (usually Prop)
  let tt ← inferType t
  let v := (← inferType tt).sortLevel!
  let rw_eq := mkApp6 -- build `@congrArg.{u,v} α tt a b f eq : f a = f b`
    (mkConst ``congrArg [u,v])
    α tt a b f eq
  logInfo m!"rw_eq := {rw_eq}"
  -- We know `f a = t` (definitionally)
  -- Now let's calculate `f b` using famous "beta reduction".
  let fb := f.beta #[b]
  -- semantically it is the same as f.app b but the goal is not as pretty ;-)
  -- try to uncomment the following line to see the difference:
  -- let fb := f.app b
  if !tt.isSort then -- why `t` must be a sort to build an implication?
    throwError m!"Cannot convert equality between {tt} to an implication"
  -- finally, let's build the implication
  return mkApp3 -- build `Eq.mpr.{v-1} (f a) (f b) rw_eq`
    (mkConst ``Eq.mpr [tt.sortLevel!])
    t fb rw_eq

#check congrArg
#check Eq.mpr

/-
Let's test the function `proveRwImp`, and finish
the goal rewrite inside `run_tacq` script
-/
example (a b : Nat) (h : a = b) : 2*a + b = 2*b + a := by
  run_tacq goal =>
    let t := q($a + 5 - $a)
    -- try what happens with a non-prop term by uncommenting the next line
    let imp ← proveRwImp h goal.ty
    let imp_t ← inferType imp
    -- test the result of `proveRwImp`
    logInfo m!"imp : {imp_t}"
    -- finish rewriting the goal
    let mt := imp_t.bindingDomain!
    logInfo m!"Build mvar of type {mt}"
    let m ← mkFreshExprSyntheticOpaqueMVar mt
    goal.mvarId!.assign (mkApp imp m)
    replaceMainGoal [m.mvarId!]
  -- check that we have successfully rewritten the main goal
  rfl

/-
## (3) Options for normalization

There are two places where normalization happens in the code of `rw` above.
* Calling `whnf` in decomposing equality
* The `isDefEq` check to determine whether to perform the abstraction.

Full normalization is not always desired, let us see an example of what can happen.
-/
def myAdd (a b : Nat) : Nat := a + b
def myEq (a b : Nat) : Prop := (a = b)

example (a b c : Nat) (h : myEq (myAdd a b) (myAdd b c))
    (h2 : a + b = c) : True := by
  run_tacq
    let htn ← whnf h.ty
    logInfo m!"{h.ty}\n→ {htn}" -- whnf unpacks `myEq`
    let (u,α,lhs,rhs) ← decomposeEq h
    logInfo m!"lhs := {lhs}, rhs := {rhs}" -- so `decomposeEq` splits the equality
    -- This is a nice feature :-)

    -- similarly isDefEq unpacks the definition
    logInfo m!"({h.ty}) ?= ({htn}) : {← isDefEq h.ty htn}"
    -- so myAbstract catches such occurence too
    logInfo m!"Abstracting ({lhs}) in ({h2.ty}):\n{← abstractToMapping h2.ty lhs}"
    -- This is rather confusing, we see no `(myAdd a b)` in `a + b = c`.
  trivial

-- In both cases, normalization is performed based on the Meta.Config
#check Meta.Config
/-
To see all the options, Ctrl-click on that type to see all the options with their
explanation. There are many options to look at such as `beta`, `zeta`, `zetaDelta`.
For example, to prevent `1 + 3` being identified with 4, we need
`offsetCnstrs := false`

Here, we focus on the example of transparency -- expanding definitions.
-/
#check Meta.Config.transparency
-- The most typical options are
#check TransparencyMode.default
#check TransparencyMode.reducible -- prevent the above definition expansion

/-
Let us see how to set reducible transparency. The config lives
in a Reader monad, meaning it is read-only. We cannot change the config
for the outside code but we can run a local piece of code with a changed config.
-/
#check withConfig
#check withTransparency

example (a b : Nat) (h1 : a = b) (h2 : myEq a b) : True := by
  run_tacq
    logInfo m!"isDefEq default: {← isDefEq h1.ty h2.ty}"
    -- general context-changing scope
    withConfig (fun cfg => {cfg with transparency := .reducible}) do
      -- anything running here has reducible trnasparency
      logInfo m!"isDefEq reducible1: {← isDefEq h1.ty h2.ty}"
    -- setting transparency in particular has a shortcut
    withTransparency .reducible do
      logInfo m!"isDefEq reducible2: {← isDefEq h1.ty h2.ty}"
    -- we should do this when matching `lhs` to an expression

    -- the same works with whnf
    logInfo m!"whnf default: {← whnf h2.ty}"
    withTransparency .reducible do
      logInfo m!"whnf reducible1: {← whnf h2.ty}"
    -- `whnfR` is a shortcut for above
    logInfo m!"whnf reducible2: {← whnfR h2.ty}"
  trivial

/-
## (4) Unification - rewriting a quantified equality.

Now, we want to be able to rewrite quantified equality, for example we have a rule
`∀ a b : Nat, p a + b = a + q b`,
and we want to use it to rewrite `p 1 + 2` to `1 + q 2`.
The main idea is to first replace the quantified variables with metavariables to obtain
`p ?a + ?b = p ?b + ?a`
Now the left hand side is structurally the same as `p 1 + 2` up to the
metavariables, so we need to find the right value for them.

### Unification

Finding values for metavariables actually happens automatically:
-/
example (p : Nat → Nat) : True := by
  run_tacq
    let a ← mkFreshExprMVarQ q(Nat) (userName := `a) -- just a Qq-sugar on top
    let b ← mkFreshExprMVarQ q(Nat) (userName := `b) -- of `mkFreshExprMVar`
    let pattern := q($p $a + $b)
    let rhs := q($a + $p $b)
    let target := q($p 1 + 2)
    logInfo m!"matching {target} with {pattern}\n→ {rhs}" -- not assigned
    if ← isDefEq pattern target then -- The magic happens here!
      logInfo m!"matched {target} with {pattern}\n→ {rhs}" -- assigned
    else
      logInfo "Couldn't match"
    logInfo m!"If match succeeded, the mvars are now assigned: a = {a}, b = {b}"
    -- Note that using MessageData is crucial - the expressions `a`, `b` didn't change,
    -- only the way we look at them inside MetaM Monad.
    logInfo s!"As a string, we still see the metavariables:\na = {a}, b = {b}"
    let a2 ← instantiateMVars a
    let b2 ← instantiateMVars b
    logInfo s!"Unless we instantiate:\na2 = {a2}, b2 = {b2}"
  trivial

/-
As we saw, `isDefEq` is not just an innocent check, it can modify the proofstate
by assiging metavariables.
Besides checking modulo basic reductions, it tries to find variable assignment that
satisfies the equality. If there exist an assignment, the asignment is performed in
the proofstate, and `isDefEq` return `true`. On the other hand, if the return value
is `false`, we know there was no change in the proof state.

### Controling assignable meta-variables

There are two factors deciding whether a metavariable can be automatically
assigned with `isDefEq`. We have already seen the meta-variable kind.
-/
run_meta
  let mNat1 ← mkFreshExprMVarQ q(Nat) .natural `mNat1
  let mNat2 ← mkFreshExprMVarQ q(Nat) .natural `mNat2
  let mSyn1 ← mkFreshExprMVarQ q(Nat) .synthetic `mSyn1
  let mSyn2 ← mkFreshExprMVarQ q(Nat) .synthetic `mSyn2
  -- natural mvars prefer to be assigned over synthetic
  logInfo m!"mNat1 = mSyn1: {← isDefEq mNat1 mSyn1}"
  logInfo m!"mNat2 = mSyn2: {← isDefEq mNat2 mSyn2}"
  logInfo m!"{mNat1} = {mSyn1}, {mNat2} = {mSyn2}"
  -- but synthetic can be assigned too if needed
  logInfo m!"mSyn1 = mSyn2: {← isDefEq mSyn1 mSyn2}"
  logInfo m!"{mSyn1} = {mSyn2}"
  -- contrary to synthetic opaque
  let mSO1 ← mkFreshExprMVarQ q(Nat) .syntheticOpaque `mSO1
  let mSO2 ← mkFreshExprMVarQ q(Nat) .syntheticOpaque `mSO2
  logInfo m!"mSO1 = mSO2: {← isDefEq mSO1 mSO2}"

/-
Often, we also want to prevent previously created mvars to be assigned no matter
of their kind. This can be done by entering a new `MetavarContext.depth`.
-/
#check withNewMCtxDepth
run_meta
  let mNat1 ← mkFreshExprMVarQ q(Nat) .natural `mNat1
  let mNat2 ← mkFreshExprMVarQ q(Nat) .natural `mNat2
  -- normally, mNat would be prefered to be assigned,
  -- we block it by entering a new level
  withNewMCtxDepth do
    logInfo m!"mNat1 = mNat2: {← isDefEq mNat1 mNat2}" -- now, they cannot be assigned
    let mSyn ← mkFreshExprMVarQ q(Nat) .synthetic `mSyn
    -- but a new synthetic can be assigned to them
    logInfo m!"mSyn = mNat1: {← isDefEq mSyn mNat1}"
    logInfo m!"{mSyn} = {mNat1}"
/-

### Quantified `rw`

Since we used `isDefEq` in `myAbstract`, it should be no surprise now why
rewriting with a quantified equality matches its first instantiation.
The unification happens the first moment it can, and later `f a` cannot
get unified with `f b`.

Let us finish the implementation by turning quantifiers into metavariables.
The function to do it is `forallMetaTelescope`
-/
#check forallMetaTelescope
-- rewrite a quantified equality
example (a b : Nat) (f : Nat → Nat) (p : Nat → Prop)
    (h_eq  : ∀ x : Nat, f x = x) (h_finish : p (a + b + a + b)) :
    p (f a + f b + f a + f b) := by
  run_tacq goal =>
    let imp ← withNewMCtxDepth do
      let (mvars, _, eq) ← forallMetaTelescope h_eq.ty -- turn quantifiers into mvars
      let pf_eq := mkAppN h_eq mvars -- build the proof term
      logInfo m!"before: {pf_eq} : {eq}"
      -- the same rewrite code as before
      let imp ← withTransparency .reducible <| proveRwImp pf_eq goal.ty
      logInfo m!"after: {pf_eq} : {eq}"
      -- we need to instantiate the variables before exiting the MCtx context
      instantiateMVars imp
    let imp_t ← inferType imp
    logInfo m!"imp_t2 := {imp_t}"
    let mt := imp_t.bindingDomain!
    let m ← mkFreshExprSyntheticOpaqueMVar mt
    goal.mvarId!.assign (mkApp imp m)
    replaceMainGoal [m.mvarId!]
  -- successfully rewritten `f a` → `a`
  rw [h_eq b] -- let's do the second step with a simple `rw` :-)
  trivial

-- Exercise: Implement the full `my_rw` tactic
example (a b : Nat) (f : Nat → Nat) (p : Nat → Prop)
    (h_eq  : ∀ x : Nat, f x = x) (h_finish : p (a + b + a + b)) :
    p (f a + f b + f a + f b) := by
  my_rw h_eq
  my_rw h_eq
  exact h_finish
