import BidirTT.Context
import BidirTT.Eval
import BidirTT.Pretty

namespace BidirTT

abbrev TCM := Except String

private def showTy (l : Lvl) (v : Val) : String :=
  match quote l v with
  | Except.ok tm     => prettyTm tm
  | Except.error err => s!"<ill-scoped value: {err}>"

mutual
  partial def check (cxt : Cxt) : Raw → Val → TCM Tm
    | .lam x t, .pi a c => do
        let bodyTy ← cApp c (.neu (.var cxt.lvl))
        let t' ← check (cxt.bind x a) t bodyTy
        pure (.lam t')
    | .pair t u, .sig a c => do
        let t' ← check cxt t a
        let vt ← eval cxt.env t'
        let bodyTy ← cApp c vt
        let u' ← check cxt u bodyTy
        pure (.pair t' u')
    | .refl, .id a t u => do
        if (← conv cxt.lvl t u) then
          pure .refl
        else
          throw s!"refl cannot inhabit {showTy cxt.lvl (.id a t u)} because the endpoints are not definitionally equal"
    | .letE x a t u, ty => do
        let (a', _) ← inferUniverse cxt a
        let va := eval cxt.env a'
        let va ← va
        let t' ← check cxt t va
        let vt ← eval cxt.env t'
        let u' ← check (cxt.define x vt va) u ty
        pure (.letE a' t' u')
    | r, ty => do
        let (t', ty') ← infer cxt r
        if (← sub cxt.lvl ty' ty) then
          pure t'
        else
          throw s!"type mismatch: expected {showTy cxt.lvl ty}, got {showTy cxt.lvl ty'}"

  partial def infer (cxt : Cxt) : Raw → TCM (Tm × Val)
    | .var x =>
        match cxt.lookup x with
        | some (i, a) => pure (.var i.val, a)
        | none        => throw s!"unknown variable {x}"
    | .nat => pure (.nat, .univ 0)
    | .zero => pure (.zero, .nat)
    | .succ t => do
        let t' ← check cxt t .nat
        pure (.succ t', .nat)
    | .natElim n motive z k ih s scrut => do
        let scrut' ← check cxt scrut .nat
        let vscrut ← eval cxt.env scrut'
        let (motiveBody', level) ← inferUniverse (cxt.bind n .nat) motive
        let motive' : Tm := .lam motiveBody'
        let vmotive ← eval cxt.env motive'
        let zTy ← vApp vmotive .zero
        let z' ← check cxt z zTy
        let kCxt := cxt.bind k .nat
        let ihTy ← vApp vmotive (.neu (.var cxt.lvl))
        let stepTy ← vApp vmotive (.succ (.neu (.var cxt.lvl)))
        let stepBody' ← check (kCxt.bind ih ihTy) s stepTy
        let step' : Tm := .lam (.lam stepBody')
        let resultTy ← vApp vmotive vscrut
        let _ := level
        pure (.natElim motive' z' step' scrut', resultTy)
    | .unit => pure (.unit, .univ 0)
    | .triv => pure (.triv, .unit)
    | .unitElim u motive t scrut => do
        let scrut' ← check cxt scrut .unit
        let vscrut ← eval cxt.env scrut'
        let (motiveBody', level) ← inferUniverse (cxt.bind u .unit) motive
        let motive' : Tm := .lam motiveBody'
        let vmotive ← eval cxt.env motive'
        let tTy ← vApp vmotive .triv
        let t' ← check cxt t tTy
        let resultTy ← vApp vmotive vscrut
        let _ := level
        pure (.unitElim motive' t' scrut', resultTy)
    | .empty => pure (.empty, .univ 0)
    | .emptyElim e motive scrut => do
        let scrut' ← check cxt scrut .empty
        let vscrut ← eval cxt.env scrut'
        let (motiveBody', level) ← inferUniverse (cxt.bind e .empty) motive
        let motive' : Tm := .lam motiveBody'
        let vmotive ← eval cxt.env motive'
        let resultTy ← vApp vmotive vscrut
        let _ := level
        pure (.emptyElim motive' scrut', resultTy)
    | .id a t u => do
        let (a', level) ← inferUniverse cxt a
        let va ← eval cxt.env a'
        let t' ← check cxt t va
        let u' ← check cxt u va
        pure (.id a' t' u', .univ level)
    | .refl => throw "cannot infer type of refl, use an annotation"
    | .idElim y p motive r target eq => do
        let (eq', eqTy) ← infer cxt eq
        match eqTy with
        | .id a x rhs => do
            let (target', targetTy) ← infer cxt target
            if !(← conv cxt.lvl targetTy a) then
              throw s!"idElim target has type {showTy cxt.lvl targetTy}, but the equality lives over {showTy cxt.lvl a}"
            let vtarget ← eval cxt.env target'
            if !(← conv cxt.lvl vtarget rhs) then
              throw s!"idElim target {showTy cxt.lvl vtarget} does not match the equality endpoint {showTy cxt.lvl rhs}"
            let eqVarTy : Val := .id a x (.neu (.var cxt.lvl))
            let (motiveBody', level) ← inferUniverse ((cxt.bind y a).bind p eqVarTy) motive
            let motive' : Tm := .lam (.lam motiveBody')
            let vmotive ← eval cxt.env motive'
            let reflTy ← vApp vmotive x
            let reflTy ← vApp reflTy .refl
            let r' ← check cxt r reflTy
            let veq ← eval cxt.env eq'
            let resultTy ← vApp vmotive vtarget
            let resultTy ← vApp resultTy veq
            let _ := level
            pure (.idElim motive' r' target' eq', resultTy)
        | _ => throw s!"expected Id type in idElim, got {showTy cxt.lvl eqTy}"
    | .univ i => pure (.univ i.normalise, .univ i.succ')
    | .app t u => do
        let (t', tty) ← infer cxt t
        match tty with
        | .pi a c => do
            let u' ← check cxt u a
            let vu ← eval cxt.env u'
            let bodyTy ← cApp c vu
            pure (.app t' u', bodyTy)
        | _ => throw s!"expected Pi type in application, got {showTy cxt.lvl tty}"
    | .fst t => do
        let (t', tty) ← infer cxt t
        match tty with
        | .sig a _ => pure (.fst t', a)
        | _ => throw s!"expected Sigma type in .1, got {showTy cxt.lvl tty}"
    | .snd t => do
        let (t', tty) ← infer cxt t
        match tty with
        | .sig _ c => do
            let vt ← eval cxt.env t'
            let fstv ← vFst vt
            let bodyTy ← cApp c fstv
            pure (.snd t', bodyTy)
        | _ => throw s!"expected Sigma type in .2, got {showTy cxt.lvl tty}"
    | .pi x a b => do
        let (a', i) ← inferUniverse cxt a
        let va ← eval cxt.env a'
        let (b', j) ← inferUniverse (cxt.bind x va) b
        pure (.pi a' b', .univ (i.max' j))
    | .sig x a b => do
        let (a', i) ← inferUniverse cxt a
        let va ← eval cxt.env a'
        let (b', j) ← inferUniverse (cxt.bind x va) b
        pure (.sig a' b', .univ (i.max' j))
    | .ann t a => do
        let (a', _) ← inferUniverse cxt a
        let va ← eval cxt.env a'
        let t' ← check cxt t va
        pure (t', va)
    | .letE x a t u => do
        let (a', _) ← inferUniverse cxt a
        let va ← eval cxt.env a'
        let t' ← check cxt t va
        let vt ← eval cxt.env t'
        let (u', uty) ← infer (cxt.define x vt va) u
        pure (.letE a' t' u', uty)
    | .lam _ _  => throw "cannot infer type of lambda, use an annotation"
    | .pair _ _ => throw "cannot infer type of pair, use an annotation"

  partial def inferUniverse (cxt : Cxt) (r : Raw) : TCM (Tm × Level) := do
    let (t, ty) ← infer cxt r
    match ty with
    | .univ level => pure (t, level)
    | _ => throw s!"expected a universe, got {showTy cxt.lvl ty}"
end

def checkTop (r : Raw) : TCM (Tm × Val) :=
  infer Cxt.empty r

end BidirTT
