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

namespace Lean

structure ClassEntry where
  name : Name
  hasOutParam : Bool

namespace ClassEntry

def lt (a b : ClassEntry) : Bool :=
  Name.quickLt a.name b.name

end ClassEntry

structure ClassState where
  hasOutParam : SMap Name Bool := SMap.empty
  deriving Inhabited

namespace ClassState

def addEntry (s : ClassState) (entry : ClassEntry) : ClassState :=
  { s with hasOutParam := s.hasOutParam.insert entry.name entry.hasOutParam }

def switch (s : ClassState) : ClassState :=
  { s with hasOutParam := s.hasOutParam.switch }

end ClassState

/- TODO: add support for scoped instances -/
builtin_initialize classExtension : SimplePersistentEnvExtension ClassEntry ClassState ←
  registerSimplePersistentEnvExtension {
    name          := `classExt
    addEntryFn    := ClassState.addEntry
    addImportedFn := fun es => (mkStateFromImportedEntries ClassState.addEntry {} es).switch
  }

@[export lean_is_class]
def isClass (env : Environment) (n : Name) : Bool :=
  (classExtension.getState env).hasOutParam.contains n

@[export lean_has_out_params]
def hasOutParams (env : Environment) (n : Name) : Bool :=
  match (classExtension.getState env).hasOutParam.find? n with
  | some b => b
  | none   => false

@[export lean_is_out_param]
def isOutParam (e : Expr) : Bool :=
  e.isAppOfArity `outParam 1

/--
  Auxiliary function for checking whether a class has `outParam`, and
  whether they are being correctly used.
  A regular (i.e., non `outParam`) must not depend on an `outParam`.
  Reason for this restriction:
  When performing type class resolution, we replace arguments that
  are `outParam`s with fresh metavariables. If regular parameters could
  depend on `outParam`s, then we would also have to replace them with
  fresh metavariables. Otherwise, the resulting expression could be type
  incorrect. This transformation would be counterintuitive to users since
  we would implicitly treat these regular parameters as `outParam`s.
-/
private partial def checkOutParam : Nat → Array FVarId → Expr → Except String Bool
  | i, outParams, Expr.forallE _ d b _ =>
    if isOutParam d then
      let fvarId    := Name.mkNum `_fvar outParams.size
      let outParams := outParams.push fvarId
      let fvar      := mkFVar fvarId
      let b         := b.instantiate1 fvar
      checkOutParam (i+1) outParams b
    else if d.hasAnyFVar fun fvarId => outParams.contains fvarId then
      Except.error s!"invalid class, parameter #{i} depends on `outParam`, but it is not an `outParam`"
    else
      checkOutParam (i+1) outParams b
  | i, outParams, e => pure (outParams.size > 0)

def addClass (env : Environment) (clsName : Name) : Except String Environment :=
  if isClass env clsName then
    Except.error s!"class has already been declared '{clsName}'"
  else match env.find? clsName with
    | none      => Except.error ("unknown declaration '" ++ toString clsName ++ "'")
    | some decl@(ConstantInfo.inductInfo _) => do
      let b ← checkOutParam 1 #[] decl.type
      Except.ok (classExtension.addEntry env { name := clsName, hasOutParam := b })
    | some _    => Except.error ("invalid 'class', declaration '" ++ toString clsName ++ "' must be inductive datatype or structure")

private def consumeNLambdas : Nat → Expr → Option Expr
  | 0,   e                => some e
  | i+1, Expr.lam _ _ b _ => consumeNLambdas i b
  | _,   _                => none

partial def getClassName (env : Environment) : Expr → Option Name
  | Expr.forallE _ _ b _ => getClassName env b
  | e                    => do
    let Expr.const c _ _ ← pure e.getAppFn | none
    let info ← env.find? c
    match info.value? with
    | some val => do
      let body ← consumeNLambdas e.getAppNumArgs val
      getClassName env body
    | none =>
      if isClass env c then some c
      else none

builtin_initialize
  registerBuiltinAttribute {
    name  := `class,
    descr := "type class",
    add   := fun decl args kind => do
      let env ← getEnv
      if args.hasArgs then throwError "invalid attribute 'class', unexpected argument"
      unless kind == AttributeKind.global do throwError "invalid attribute 'class', must be global"
      let env ← ofExcept (addClass env decl)
      setEnv env
  }

end Lean
