import Lean

mutual

inductive Vect (α : Type u) (β : Type v) : Nat → Nat → Type max u v where
  | nil   : Vect α β 0 0
  | lst   : List (Array (Vect α β 0 0)) → Vect α β 0 0
  | cons1 : {n m : Nat} → TreeV α β n → Vect α β n m → Vect α β (n+1) m
  | cons2 : {n m : Nat} → TreeV α β m → Vect α β n m → Vect α β n (m+1)

inductive TreeV (α : Type u) (β : Type v) : Nat → Type max u v where
  | diag   : {n : Nat}   → Vect α β n n → TreeV α β n
  | lower  : {n m : Nat} → n < m → Vect α β n m → TreeV α β m

end

mutual

partial def aux1 {α β m n} [ToString α] [ToString β] (s : Vect α β m n) : String :=
  let inst1 {m' n' : Nat} : ToString (Vect α β m' n') := ⟨aux1⟩
  let inst1 {n' : Nat}    : ToString (TreeV α β n')   := ⟨aux2⟩
  match m, n, s with
  | _, _, Vect.nil       => "nil"
  | _, _, Vect.lst xs    => "lst " ++ toString xs
  | _, _, Vect.cons1 t v => "cons1 " ++ toString t ++ " " ++ toString v
  | _, _, Vect.cons2 t v => "cons2 " ++ toString t ++ " " ++ toString v


partial def aux2 {α β n} [ToString α] [ToString β] (s : TreeV α β n) : String :=
  let inst1 {m' n' : Nat} : ToString (Vect α β m' n') := ⟨aux1⟩
  let inst1 {n' : Nat}    : ToString (TreeV α β n')   := ⟨aux2⟩
  match n, s with
  | _, TreeV.diag v    => "diag  " ++ toString v
  | _, TreeV.lower _ v => "lower " ++ "<proof>" ++ " " ++ toString v

end

instance {α β m n} [ToString α] [ToString β] : ToString (Vect α β m n) where
  toString := aux1

instance {α β n} [ToString α] [ToString β] : ToString (TreeV α β n) where
  toString := aux2


namespace Test
mutual
inductive Foo (α : Type) where
  | mk : List (Bla α) → Foo α
  | leaf : α → Foo α
inductive Bla (α : Type) where
  | nil : Bla α
  | cons : Foo α → Bla α → Bla α
end


open Lean
open Lean.Meta

def tst : MetaM Unit := do
  let info ← getConstInfoInduct `Test.Bla
  trace[Meta.debug] "nested: {info.isNested}"
  pure ()

#eval tst

mutual

partial def fooToString {α : Type} [ToString α] (s : Foo α) : String :=
  let inst1 : ToString (Bla α) := ⟨blaToString⟩
  match s with
  | Foo.mk bs  => "Foo.mk " ++ toString bs
  | Foo.leaf a => "Foo.leaf " ++ toString a

partial def blaToString {α : Type} [ToString α] (s : Bla α) : String :=
  let inst1 : ToString (Bla α) := ⟨blaToString⟩
  let inst2 : ToString (Foo α) := ⟨fooToString⟩
  match s with
  | Bla.nil      => "Bla.nil"
  | Bla.cons h t => "Bla.cons (" ++ toString h ++ ") " ++ toString t

end

instance [ToString α] : ToString (Foo α) where
  toString := fooToString

instance [ToString α] : ToString (Bla α) where
  toString := blaToString

#eval Foo.mk [Bla.cons (Foo.leaf 10) Bla.nil]

end Test


namespace Lean.Elab
open Meta

namespace Deriving

/- For type classes such as BEq, ToString, Format -/
namespace Default

structure ContextCore where
  classInfo   : ConstantInfo
  typeInfos   : Array InductiveVal
  auxFunNames : Array Name
  usePartial  : Bool
  resultType  : Syntax

structure Header where
  binders    : Array Syntax
  argNames   : Array Name
  targetName : Name

abbrev MkAltRhs :=
  ContextCore → (ctorName : Name) → (ctorArgs : Array (Syntax × Expr)) → TermElabM Syntax

structure Context extends ContextCore where
  mkAltRhs : MkAltRhs

def mkContext (className : Name) (typeName : Name) (resultType : Syntax) (mkAltRhs : MkAltRhs) : TermElabM Context := do
  let indVal ← getConstInfoInduct typeName
  let mut typeInfos := #[]
  for typeName in indVal.all do
    typeInfos ← typeInfos.push (← getConstInfoInduct typeName)
  let classInfo ← getConstInfo className
  let mut auxFunNames := #[]
  for typeName in indVal.all do
    match className.eraseMacroScopes, typeName.eraseMacroScopes with
    | Name.str _ c _, Name.str _ t _ => auxFunNames := auxFunNames.push (← mkFreshUserName <| Name.mkSimple <| c.decapitalize ++ t)
    | _, _ => auxFunNames := auxFunNames.push (← mkFreshUserName `instFn)
  trace[Meta.debug] "{auxFunNames}"
  let usePartial := indVal.isNested || typeInfos.size > 1
  return {
    classInfo   := classInfo
    typeInfos   := typeInfos
    auxFunNames := auxFunNames
    usePartial  := usePartial
    resultType  := resultType
    mkAltRhs    := mkAltRhs
  }

def mkInductArgNames (indVal : InductiveVal) : TermElabM (Array Name) := do
  forallTelescopeReducing indVal.type fun xs _ => do
    let mut argNames := #[]
    for x in xs do
      let localDecl ← getLocalDecl x.fvarId!
      let paramName ← mkFreshUserName localDecl.userName.eraseMacroScopes
      argNames := argNames.push paramName
    pure argNames

def mkInductiveApp (indVal : InductiveVal) (argNames : Array Name) : TermElabM Syntax :=
  let f    := mkIdent indVal.name
  let args := argNames.map mkIdent
  `(@$f $args*)

def implicitBinderF := Parser.Term.implicitBinder
def instBinderF     := Parser.Term.instBinder
def explicitBinderF := Parser.Term.explicitBinder

def mkImplicitBinders (argNames : Array Name) : TermElabM (Array Syntax) :=
  argNames.mapM fun argName =>
    `(implicitBinderF| { $(mkIdent argName) })

def mkInstImplicitBinders (ctx : Context) (indVal : InductiveVal) (argNames : Array Name) : TermElabM (Array Syntax) :=
  forallBoundedTelescope indVal.type indVal.nparams fun xs _ => do
    let mut binders := #[]
    for i in [:xs.size] do
      try
        let x := xs[i]
        let clsName := ctx.classInfo.name
        let c ← mkAppM clsName #[x]
        if (← isTypeCorrect c) then
          let argName := argNames[i]
          let binder ← `(instBinderF| [ $(mkIdent clsName):ident $(mkIdent argName):ident ])
          binders := binders.push binder
      catch _ =>
        pure ()
    return binders

def mkHeader (ctx : Context) (indVal : InductiveVal) : TermElabM Header := do
  let argNames     ← mkInductArgNames indVal
  let binders      ← mkImplicitBinders argNames
  let targetType   ← mkInductiveApp indVal argNames
  let targetName   ← mkFreshUserName `x
  let targetBinder ← `(explicitBinderF| ($(mkIdent targetName) : $targetType))
  let binders      := binders ++ (← mkInstImplicitBinders ctx indVal argNames)
  let binders      := binders.push targetBinder
  return {
    binders    := binders
    argNames   := argNames
    targetName := targetName
  }

def mkLocalInstanceLetDecls (ctx : Context) (argNames : Array Name) : TermElabM (Array Syntax) := do
  let mut letDecls := #[]
  for i in [:ctx.typeInfos.size] do
    let indVal       := ctx.typeInfos[i]
    let auxFunName   := ctx.auxFunNames[i]
    let currArgNames ← mkInductArgNames indVal
    let numParams    := indVal.nparams
    let currIndices  := currArgNames[numParams...*]
    let binders      ← mkImplicitBinders currIndices
    let argNamesNew  := argNames[:numParams] ++ currIndices
    let indType      ← mkInductiveApp indVal argNamesNew
    let type         ← `($(mkIdent ctx.classInfo.name) $indType)
    let val          ← `(⟨$(mkIdent auxFunName)⟩)
    let instName     ← mkFreshUserName `localinst
    let letDecl      ← `(Parser.Term.letDecl| $(mkIdent instName):ident $binders:implicitBinder* : $type := $val)
    letDecls := letDecls.push letDecl
  return letDecls

def mkLet (letDecls : Array Syntax) (body : Syntax) : TermElabM Syntax :=
  letDecls.foldrM (init := body) fun letDecl body =>
    `(let $letDecl:letDecl; $body)

def matchAltExpr := Parser.Term.matchAlt

def mkMatch (ctx : Context) (header : Header) (indVal : InductiveVal) (argNames : Array Name) : TermElabM Syntax := do
  let discrs ← mkDiscrs
  let alts ← mkAlts
  `(match $[$discrs],* with | $[$alts:matchAlt]|*)
where
  mkDiscr (varName : Name) : TermElabM Syntax :=
    `(Parser.Term.matchDiscr| $(mkIdent varName):term)

  mkDiscrs : TermElabM (Array Syntax) := do
    let mut discrs := #[]
    -- add indices
    for argName in argNames[indVal.nparams:] do
      discrs := discrs.push (← mkDiscr argName)
    return discrs.push (← mkDiscr header.targetName)

  mkAlts : TermElabM (Array Syntax) := do
    let mut alts := #[]
    for ctorName in indVal.ctors do
      let mut patterns := #[]
      -- add `_` pattern for indices
      for i in [:indVal.nindices] do
        patterns := patterns.push (← `(_))
      let ctorInfo ← getConstInfoCtor ctorName
      let mut ctorArgs := #[]
      -- add `_` for inductive parameters, they are inaccessible
      for i in [:indVal.nparams] do
        ctorArgs := ctorArgs.push (← `(_))
      for i in [:ctorInfo.nfields] do
        ctorArgs := ctorArgs.push (mkIdent (← mkFreshUserName `y))
      patterns := patterns.push (← `(@$(mkIdent ctorName):ident $ctorArgs:term*))
      let altRhs ← forallTelescopeReducing ctorInfo.type fun xs _ =>
        ctx.mkAltRhs ctx.toContextCore ctorName (Array.zip ctorArgs xs)
      let alt ← `(matchAltExpr| $[$patterns:term],* => $altRhs:term)
      alts := alts.push alt
    return alts

def mkAuxFunction (ctx : Context) (i : Nat) : TermElabM Syntax := do
  let auxFunName ← ctx.auxFunNames[i]
  let indVal     ← ctx.typeInfos[i]
  let header     ← mkHeader ctx indVal
  let mut body   ← mkMatch ctx header indVal header.argNames
  if ctx.usePartial then
    let letDecls ← mkLocalInstanceLetDecls ctx header.argNames
    body ← mkLet letDecls body
  let binders    := header.binders
  if ctx.usePartial then
    `(private partial def $(mkIdent auxFunName):ident $binders:explicitBinder* : $ctx.resultType:term := $body:term)
  else
    `(private def $(mkIdent auxFunName):ident $binders:explicitBinder* : $ctx.resultType:term := $body:term)

def mkMutualBlock (ctx : Context) : TermElabM Syntax := do
  let mut auxDefs := #[]
  for i in [:ctx.typeInfos.size] do
    auxDefs := auxDefs.push (← mkAuxFunction ctx i)
  `(mutual $auxDefs:command* end)

def mkInstanceCmds (ctx : Context) : TermElabM (Array Syntax) := do
  let mut instances := #[]
  for i in [:ctx.typeInfos.size] do
    let indVal       := ctx.typeInfos[i]
    let auxFunName   := ctx.auxFunNames[i]
    let argNames     ← mkInductArgNames indVal
    let binders      ← mkImplicitBinders argNames
    let binders      := binders ++ (← mkInstImplicitBinders ctx indVal argNames)
    let indType      ← mkInductiveApp indVal argNames
    let type         ← `($(mkIdent ctx.classInfo.name) $indType)
    let val          ← `(⟨$(mkIdent auxFunName)⟩)
    let instCmd ← `(instance $binders:implicitBinder* : $type := $val)
    trace[Meta.debug] "\n{instCmd}"
    instances := instances.push instCmd
  return instances

open Command

def mkDeriving (className : Name) (typeName : Name) (resultType : Syntax) (mkAltRhs : MkAltRhs) : CommandElabM Unit := do
  let cmds ← liftTermElabM none do
    let ctx ← mkContext className typeName resultType mkAltRhs
    let block ← mkMutualBlock ctx
    trace[Meta.debug] "\n{block}"
    return #[block] ++ (← mkInstanceCmds ctx)
  cmds.forM elabCommand

def mkDerivingToString (typeName : Name) : CommandElabM Unit := do
  mkDeriving `ToString typeName (← `(String)) fun ctx ctorName ctorArgs =>
    quote (toString ctorName) -- TODO

syntax[runTstKind] "runTst" : command

@[command_elab runTstKind] def elabTst : CommandElab := fun stx =>
  mkDerivingToString `Test.Foo

set_option trace.Meta.debug true
runTst
