/-
Copyright (c) 2020 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Lean.Meta.KAbstract
import Lean.Meta.Tactic.Util
import Lean.Meta.Tactic.Intro
import Lean.Meta.Tactic.FVarSubst
import Lean.Meta.Tactic.Revert

namespace Lean.Meta

/-- The `generalize` tactic takes arguments of the form `h : e = x` -/
structure GeneralizeArg where
  expr   : Expr
  xName? : Option Name := none
  hName? : Option Name := none
  deriving Inhabited

/--
Telescopic `generalize` tactic. It can simultaneously generalize many terms.
It uses `kabstract` to occurrences of the terms that need to be generalized.
-/
private partial def generalizeCore (mvarId : MVarId) (args : Array GeneralizeArg) : MetaM (Array FVarId × MVarId) :=
  mvarId.withContext do
    mvarId.checkNotAssigned `generalize
    let tag ← mvarId.getTag
    let target ← instantiateMVars (← mvarId.getType)
    let rec go (i : Nat) : MetaM Expr := do
      if _h : i < args.size then
        let arg := args[i]
        let e ← instantiateMVars arg.expr
        let eType ← instantiateMVars (← inferType e)
        let type ← go (i+1)
        let xName ← if let some xName := arg.xName? then pure xName else mkFreshUserName `x
        return Lean.mkForall xName BinderInfo.default eType (← kabstract type e)
      else
        return target
    let targetNew ← go 0
    unless (← isTypeCorrect targetNew) do
      throwTacticEx `generalize mvarId m!"result is not type correct{indentExpr targetNew}"
    let es := args.map (·.expr)
    if !args.any fun arg => arg.hName?.isSome then
      let mvarNew ← mkFreshExprSyntheticOpaqueMVar targetNew tag
      mvarId.assign (mkAppN mvarNew es)
      mvarNew.mvarId!.introNP args.size
    else
      let (rfls, targetNew) ← forallBoundedTelescope targetNew args.size fun xs type => do
        let rec go' (i : Nat) : MetaM (List Expr × Expr) := do
          if _h : i < xs.size then
            let arg := args[i]!
            if let some hName := arg.hName? then
              let xType ← inferType xs[i]
              let e ← instantiateMVars arg.expr
              let eType ← instantiateMVars (← inferType e)
              let (hType, r) ← if (← isDefEq xType eType) then
                pure (← mkEq e xs[i], ← mkEqRefl e)
              else
                pure (← mkHEq e xs[i], ← mkHEqRefl e)
              let (rs, type) ← go' (i+1)
              return (r :: rs, mkForall hName BinderInfo.default hType type)
            else
              go' (i+1)
          else
            return ([], type)
        let (rfls, type) ← go' 0
        return (rfls, ← mkForallFVars xs type)
      let mvarNew ← mkFreshExprSyntheticOpaqueMVar targetNew tag
      mvarId.assign (mkAppN (mkAppN mvarNew es) rfls.toArray)
      mvarNew.mvarId!.introNP (args.size + rfls.length)

@[inherit_doc generalizeCore]
def _root_.Lean.MVarId.generalize (mvarId : MVarId) (args : Array GeneralizeArg) : MetaM (Array FVarId × MVarId) :=
  generalizeCore mvarId args

@[inherit_doc generalizeCore, deprecated MVarId.generalize]
def generalize (mvarId : MVarId) (args : Array GeneralizeArg) : MetaM (Array FVarId × MVarId) :=
  generalizeCore mvarId args

/--
Extension of `generalize` to support generalizing within specified hypotheses.
The `hyps` array contains the list of hypotheses within which to look for occurrences
of the generalizing expressions.
-/
def _root_.Lean.MVarId.generalizeHyp (mvarId : MVarId) (args : Array GeneralizeArg) (hyps : Array FVarId := #[])
    (fvarSubst : FVarSubst := {}) : MetaM (FVarSubst × Array FVarId × MVarId) := do
  if hyps.isEmpty then
    -- trivial case
    return (fvarSubst, ← mvarId.generalize args)
  let args ← args.mapM fun arg => return { arg with expr := ← instantiateMVars arg.expr }
  let hyps ← hyps.filterM fun h => do
    let type ← instantiateMVars (← h.getType)
    args.anyM fun arg => return (← kabstract type arg.expr).hasLooseBVars
  let (reverted, mvarId) ← mvarId.revert hyps true
  let (newVars, mvarId) ← mvarId.generalize args
  let (reintros, mvarId) ← mvarId.introNP reverted.size
  let fvarSubst := Id.run do
    let mut subst : FVarSubst := fvarSubst
    for h in reverted, reintro in reintros do
      subst := subst.insert h (mkFVar reintro)
    pure subst
  return (fvarSubst, newVars, mvarId)

end Lean.Meta
