/-
Copyright (c) 2021 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
import Lean.Parser.Term
import Lean.Elab.Term
import Lean.Expr

namespace Lake
open Lean Parser

abbrev Ellipsis := TSyntax ``Term.ellipsis
abbrev NamedArgument := TSyntax ``Term.namedArgument
abbrev Argument := TSyntax ``Term.argument

instance : Coe Term Argument where
  coe s := ⟨s.raw⟩

instance : Coe Ellipsis Argument where
  coe s := ⟨s.raw⟩

instance : Coe NamedArgument Argument where
  coe s := ⟨s.raw⟩

abbrev Hole := TSyntax ``Term.hole
abbrev BinderIdent := TSyntax ``Term.binderIdent
abbrev TypeSpec := TSyntax ``Term.typeSpec

def mkHoleFrom (ref : Syntax) : Hole :=
  mkNode ``Term.hole #[mkAtomFrom ref "_"]

instance : Coe Hole Term where
  coe s := ⟨s.raw⟩

instance : Coe Hole BinderIdent where
  coe s := ⟨s.raw⟩

instance : Coe Ident BinderIdent where
  coe s := ⟨s.raw⟩

abbrev BracketedBinder := TSyntax ``Term.bracketedBinder
abbrev FunBinder := TSyntax ``Term.funBinder

instance : Coe BinderIdent FunBinder where
  coe s := ⟨s.raw⟩

@[runParserAttributeHooks]
def binder := Term.binderIdent <|> Term.bracketedBinder

abbrev Binder := TSyntax ``binder
instance : Coe Binder (TSyntax [identKind, ``Term.hole, ``Term.bracketedBinder]) where
  coe stx := ⟨stx.raw⟩

abbrev BinderModifier := TSyntax [``Term.binderTactic, ``Term.binderDefault]

--------------------------------------------------------------------------------
-- Adapted from the private utilities in `Lean.Elab.Binders`

structure BinderSyntaxView where
  id : Ident
  type : Term
  info : BinderInfo
  modifier? : Option BinderModifier := none

def expandOptType (ref : Syntax) (optType : Syntax) : Term :=
  if optType.isNone then
    mkHoleFrom ref
  else
    ⟨optType[0][1]⟩

def getBinderIds (ids : Syntax) : MacroM (Array BinderIdent) :=
  ids.getArgs.mapM fun id =>
    let k := id.getKind
    if k == identKind || k == `Lean.Parser.Term.hole then
      return ⟨id⟩
    else
      Macro.throwErrorAt id "identifier or `_` expected"

def expandBinderIdent (stx : Syntax) : MacroM Ident :=
  match stx with
  | `(_) => (⟨·⟩) <$> Elab.Term.mkFreshIdent stx
  | _    => pure ⟨stx⟩

def expandOptIdent (stx : Syntax) : BinderIdent :=
  if stx.isNone then mkHoleFrom stx else ⟨stx[0]⟩

def expandBinderType (ref : Syntax) (stx : Syntax) : Term :=
  if stx.getNumArgs == 0 then mkHoleFrom ref else ⟨stx[1]⟩

def expandBinderModifier (optBinderModifier : Syntax) : Option BinderModifier :=
  if optBinderModifier.isNone then
    none
  else
    some ⟨optBinderModifier[0]⟩

def matchBinder (stx : Syntax) : MacroM (Array BinderSyntaxView) := do
  let k := stx.getKind
  if stx.isIdent || k == ``Term.hole then
    -- binderIdent
    return #[{ id := (← expandBinderIdent stx), type := mkHoleFrom stx, info := .default }]
  else if k == ``Lean.Parser.Term.explicitBinder then
    -- `(` binderIdent+ binderType (binderDefault <|> binderTactic)? `)`
    let ids ← getBinderIds stx[1]
    let type := stx[2]
    let modifier? := expandBinderModifier stx[3]
    ids.mapM fun id => return {
      id := ← expandBinderIdent id,
      type := expandBinderType id type,
      info := .default,
      modifier?
    }
  else if k == ``Lean.Parser.Term.implicitBinder then
    -- `{` binderIdent+ binderType `}`
    let ids ← getBinderIds stx[1]
    let type := stx[2]
    ids.mapM fun id => return {
      id := ← expandBinderIdent id,
      type := expandBinderType id type,
      info := .implicit
    }
  else if k == ``Lean.Parser.Term.strictImplicitBinder then
    -- `⦃` binderIdent+ binderType `⦄`
    let ids ← getBinderIds stx[1]
    let type := stx[2]
    ids.mapM fun id => do pure {
      id := ← expandBinderIdent id,
      type := expandBinderType id type,
      info := .strictImplicit
    }
  else if k == ``Lean.Parser.Term.instBinder then
    -- `[` optIdent type `]`
    let id := expandOptIdent stx[1]
    let type := stx[2]
    return #[{id := ← expandBinderIdent id, type := ⟨type⟩, info := .instImplicit}]
  else
    Macro.throwUnsupported

--------------------------------------------------------------------------------

def BinderSyntaxView.mkBinder : BinderSyntaxView → MacroM Binder
| {id, type, info, modifier?} => do
  match info with
  | .default        => `(binder| ($id : $type $[$modifier?]?))
  | .implicit       => `(binder| {$id : $type})
  | .strictImplicit => `(binder| ⦃$id : $type⦄)
  | .instImplicit   => `(binder| [$id : $type])
  | _               => unreachable!

def BinderSyntaxView.mkArgument : BinderSyntaxView → MacroM NamedArgument
| {id, ..} => `(Term.namedArgument| ($id := $id))

def expandBinders (dbs : Array Binder) : MacroM (Array Binder × Array Term) := do
  let mut bs := #[]
  let mut args : Array Term := #[]
  for db in dbs do
    let views ← matchBinder db.raw
    for view in views do
      bs := bs.push (← view.mkBinder)
      args := args.push ⟨(← view.mkArgument).raw⟩
  return (bs, args)
