/-
Copyright (c) 2020 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
import Lean.Message
import Lean.InternalExceptionId
import Lean.Data.Options

namespace Lean

/- Exception type used in most Lean monads -/
inductive Exception
  | error (ref : Syntax) (msg : MessageData)
  | internal (id : InternalExceptionId)

def Exception.toMessageData : Exception → MessageData
  | Exception.error _ msg => msg
  | Exception.internal id => id.toString

def Exception.getRef : Exception → Syntax
  | Exception.error ref _ => ref
  | Exception.internal _  => Syntax.missing

instance : Inhabited Exception := ⟨Exception.error (arbitrary _) (arbitrary _)⟩

class Ref (m : Type → Type) :=
  (getRef      : m Syntax)
  (withRef {α} : Syntax → m α → m α)

export Ref (getRef)

instance (m n : Type → Type) [Ref m] [MonadFunctor m n] [MonadLift m n] : Ref n := {
  getRef  := liftM (getRef : m _),
  withRef := fun ref x => monadMap (m := m) (Ref.withRef ref) x
}

def replaceRef (ref : Syntax) (oldRef : Syntax) : Syntax :=
  match ref.getPos with
  | some _ => ref
  | _      => oldRef

@[inline] def withRef {m : Type → Type} [Monad m] [Ref m] {α} (ref : Syntax) (x : m α) : m α := do
  let oldRef ← getRef
  let ref := replaceRef ref oldRef
  Ref.withRef ref x

/- Similar to `AddMessageContext`, but for error messages.
   The default instance just uses `AddMessageContext`.
   In error messages, we may want to provide additional information (e.g., macro expansion stack),
   and refine the `(ref : Syntax)`. -/
class AddErrorMessageContext (m : Type → Type) :=
  (add : Syntax → MessageData → m (Syntax × MessageData))

instance (m : Type → Type) [AddMessageContext m] [Monad m] : AddErrorMessageContext m := {
  add := fun ref msg => do
    msg ← addMessageContext msg
    pure (ref, msg)
}

section Methods

variables {m : Type → Type} [Monad m] [MonadExceptOf Exception m] [Ref m] [AddErrorMessageContext m]

def throwError {α} (msg : MessageData) : m α := do
  let ref ← getRef
  let (ref, msg) ← AddErrorMessageContext.add ref msg
  throw $ Exception.error ref msg

def throwUnknownConstant {α} (constName : Name) : m α :=
  throwError msg!"unknown constant '{mkConst constName}'"

def throwErrorAt {α} (ref : Syntax) (msg : MessageData) : m α := do
  withRef ref $ throwError msg

def ofExcept {ε α} [HasToString ε] (x : Except ε α) : m α :=
  match x with
  | Except.ok a    => pure a
  | Except.error e => throwError $ toString e

def throwKernelException {α} [MonadOptions m] (ex : KernelException) : m α := do
  throwError $ ex.toMessageData (← getOptions)

end Methods

class MonadRecDepth (m : Type → Type) :=
  (withRecDepth {α} : Nat → m α → m α)
  (getRecDepth      : m Nat)
  (getMaxRecDepth   : m Nat)

instance {ρ m} [Monad m] [MonadRecDepth m] : MonadRecDepth (ReaderT ρ m) := {
  withRecDepth   := fun d x ctx => MonadRecDepth.withRecDepth d (x ctx),
  getRecDepth    := fun _ => MonadRecDepth.getRecDepth,
  getMaxRecDepth := fun _ => MonadRecDepth.getMaxRecDepth
}

instance {ω σ m} [Monad m] [MonadRecDepth m] : MonadRecDepth (StateRefT' ω σ m) :=
  inferInstanceAs (MonadRecDepth (ReaderT _ _))

@[inline] def withIncRecDepth {α m} [Monad m] [MonadRecDepth m] [MonadExceptOf Exception m] [Ref m] [AddErrorMessageContext m]
    (x : m α) : m α := do
  let curr ← MonadRecDepth.getRecDepth
  let max  ← MonadRecDepth.getMaxRecDepth
  if curr == max then throwError maxRecDepthErrorMessage
  MonadRecDepth.withRecDepth (curr+1) x

syntax "throwError! " ((interpolatedStr term) <|> term) : term
syntax "throwErrorAt! " term:max ((interpolatedStr term) <|> term) : term

macro_rules
| `(throwError! $msg) =>
  if msg.getKind == interpolatedStrKind then
    `(throwError (msg! $msg))
  else
    `(throwError $msg)

macro_rules
| `(throwErrorAt! $ref $msg) =>
  if msg.getKind == interpolatedStrKind then
    `(throwErrorAt $ref (msg! $msg))
  else
    `(throwErrorAt $ref $msg)

end Lean
