import Specimen.LazyList
import Specimen.Utils
import Plausible.Gen

open LazyList Plausible

/-- An enumerator is a function from `Nat` to `LazyList α`, where the `Nat`
    serves an upper bound for the enumeration process, i.e. the LazyList returned
    contains all inhabitants of `α` up to the given size. -/
abbrev Enumerator (α : Type u) := Nat → LazyList α

/-- The `Enum` typeclass describes types that have an associated `Enumerator` -/
class Enum (α : Type u) where
  enum : Enumerator α

/-- The `EnumSized` typeclass describes enumerators that have an
    additional `Nat` parameter to bound their recursion depth. -/
class EnumSized (α : Type u) where
  enumSized : Nat → Enumerator α

/-- Sized enumerators of type `α` such that `P : α -> Prop` holds for all enumerated values.
    Note that these enumerators may fail, which is why they have type `ExceptT GenError Enumerator α`. -/
class EnumSizedSuchThat (α : Type) (P : α → Prop) where
  enumSizedST : Nat → ExceptT GenError Enumerator α

/-- Enumerators of type `α` such that `P : α -> Prop` holds for all generated values.
    Note that these enumerators may fail, which is why they have type `ExceptT GenError Enumerator α`. -/
class EnumSuchThat (α : Type) (P : α → Prop) where
  enumST : ExceptT GenError Enumerator α

/-- `pure x` constructs a trivial enumerator which produces a singleton `LazyList` containing `x` -/
def pureEnum (x : α) : Enumerator α :=
  fun _ => pureLazyList x

/-- Monadic-bind for enumerators -/
def bindEnum (enum : Enumerator α) (k : α → Enumerator β) : Enumerator β :=
  fun (n : Nat) => do
    let x ← enum n
    (k x) n

/-- `Monad` instance for `Enumerator`s -/
instance : Monad Enumerator where
  pure := pureEnum
  bind := bindEnum

/-- The degenerate enumerator which enumerates nothing (the empty `LazyList`) -/
def failEnum : Enumerator α :=
  fun _ => .lnil

/-- `EnumSizedSuchThat` instance for equality propositions
     where a variable `x` is left-equal to some value `val`.
    (Note: `val` can be the result of a fully-applied function application,
     which is typically how this typeclass is used!) -/
instance {α : Type} {val : α} : EnumSizedSuchThat α (fun x => x = val) where
  enumSizedST _ := return val

/-- `EnumSizedSuchThat` instance for equality propositions
     where a variable `x` is right-equal to some value `val`.
     (Note: `val` can be the result of a fully-applied function application,
     which is typically how this typeclass is used!) -/
instance {α : Type} {val : α} : EnumSizedSuchThat α (fun x => val = x) where
  enumSizedST _ := return val

/-- `Alternative` instance for `Enumerator`s.
    Note:
    - `e1 <|> e2` is not fair and is biased towards `e1`, i.e. all elements of `e1` will
      appear in the resultant enumeration before the first element of `e2`.
    - Defining a fair instance of `Alternative` requires defining an interleave operation
      on the resultant lists (see "A Completely Unique Account of Enumeration", ICFP '22),
      however it is unclear how to define an interleave operation on *LazyLists* while
      convincing Lean's termination checker to accept the definition (essentially, the
      difficulty lies in proving that forcing the thunked tail of a `LazyList` doesn't
      increase the size of the overall `LazyList`). -/
instance : Alternative Enumerator where
  failure := failEnum
  orElse e1 e2 := fun n => (e1 n) <|> (e2 () n)

/-- `sizedEnum f` constructs an enumerator that depends on `size` parameter -/
def sizedEnum (f : Nat → Enumerator α) : Enumerator α :=
  fun (n : Nat) => (f n) n

/-- Every `EnumSized` instance gives rise to an `Enum` instance -/
instance [EnumSized α] : Enum α where
  enum := sizedEnum EnumSized.enumSized

/-- Every `EnumSizedSuchThat` instance gives rise to an `EnumSuchThat` instance -/
instance [EnumSizedSuchThat α P] : EnumSuchThat α P where
  enumST := sizedEnum (EnumSizedSuchThat.enumSizedST P)

/-- Produces a `LazyList` containing all `Int`s in-between
    `lo` and `hi` (inclusive) in ascending order -/
def lazyListNatRange (lo : Nat) (hi : Nat) : LazyList Nat :=
  lazySeq .succ lo (.succ (hi - lo))

/-- Enumerates all `Nat`s in-between `lo` and `hi` (inclusive)
    in ascending order -/
def enumNatRange (lo : Nat) (hi : Nat) : Enumerator Nat :=
  fun _ => lazyListNatRange lo hi

/-- `EnumSized` instance for `Nat` -/
instance : EnumSized Nat where
  enumSized (n : Nat) := enumNatRange 0 n

namespace EnumeratorCombinators

/-- `vectorOf k e` creates an enumerator of lists of length `k`,
    where each element in the list comes from the enumerator `e` -/
def vectorOf (k : Nat) (e : Enumerator α) : Enumerator (List α) :=
  List.foldr (fun m m' => do
    let x ← m
    let xs ← m'
    return x::xs) (init := pure []) (List.replicate k e)

/-- Picks one of the enumerators in `es`, returning the `default` enumerator
    if `es` is empty. -/
def oneOfWithDefault (default : Enumerator α) (es : List (Enumerator α)) : Enumerator α :=
  match es with
  | [] => default
  | _ => do
    let idx ← enumNatRange 0 (es.length - 1)
    List.getD es idx default

/-- Picks one of the enumerators in `es`, or the `default` value if `es = []`. -/
def oneOf [Inhabited α] (es : List (Enumerator α)) : Enumerator α :=
  oneOfWithDefault (pure default) es

end EnumeratorCombinators

-- Some simple `Enum` instances

/-- `Enum` instance for `Bool` -/
instance : Enum Bool where
  enum := pureEnum false <|> pureEnum true

/-- `Enum` instance for `Option`s -/
instance [Enum α] : Enum (Option α) where
  enum := EnumeratorCombinators.oneOf [
    pure none,
    some <$> Enum.enum
  ]

/-- `Enum` instance for `Except`s, though we do not enumerate the possible exceptions thrown: typically we
  want to enumerate the "positive instances", so we simply throw `.error default` once.
-/
instance [Inhabited ε] [Enum α] : Enum (Except ε α) where
  enum := EnumeratorCombinators.oneOf [
    pure (.error default),
    .ok <$> Enum.enum
  ]

/-- `Enum` instances for pairs -/
instance [Enum α] [Enum β] : Enum (α × β) where
  enum := fun n => do
    let a ← Enum.enum n
    let b ← Enum.enum n
    pure (a, b)

/-- `Enum` instances for sums -/
instance [Enum α] [Enum β] : Enum (α ⊕ β) where
  enum := fun n =>
    (Enum.enum n >>= pure ∘ Sum.inl) <|> (Enum.enum n >>= pure ∘ Sum.inr)

/-- Produces a `LazyList` containing all `Int`s in-between
    `lo` and `hi` (inclusive) in ascending order -/
def lazyListIntRange (lo : Int) (hi : Int) : LazyList Int :=
  lazySeq (. + 1) lo (Int.toNat (hi - lo + 1))

/-- `Enum` instance for `Int` (enumerates all `int`s between `-size` and `size` inclusive) -/
instance : Enum Int where
  enum := fun size =>
    let n := Int.ofNat size
    lazyListIntRange (-n) n

/-- `EnumSized` instance for lists -/
instance [Enum α] : EnumSized (List α) where
  enumSized (n : Nat) := do
    let x ← enumNatRange 0 n
    EnumeratorCombinators.vectorOf x Enum.enum

/-- Enumerates all printable ASCII characters (codepoint 32 - 95) -/
def enumPrintableASCII (size : Nat) : LazyList Char :=
  lazySeq (fun c => Char.ofNat (c.toNat + 1)) (Char.ofNat 32) (min size 95)

/-- `Enum` instance for ASCII-printable `Char`s -/
instance : Enum Char where
  enum := enumPrintableASCII

/-- `Enum` instance for `String`s containing ASCII-printable characters -/
instance : Enum String where
  enum := String.ofList <$> (Enum.enum : Enumerator (List Char))

/-- `Enum` instance for `Fin n` where `n > 0`
  (enumerates all `Nat`s from 0 to `n - 1` inclusive) -/
instance [NeZero n] : Enum (Fin n) where
  enum := fun _ =>
    (Fin.ofNat n) <$> lazyListNatRange 0 (n - 1)

/-- `Enum` instance for `BitVec w`
    (uses the `Enum` instance for `Fin (2 ^ w)`, since bitvectors
    are represented using `Fin (2 ^ w)` under the hood) -/
instance : Enum (BitVec w) where
  enum := BitVec.ofFin <$> (Enum.enum : Enumerator (Fin (2 ^ w)))


-- Sampling from enumerators

/-- Returns a list of up to `limit` elements produced by the enumerator
    associated with the `Enum` instance for a type,
    using `size` as the size parameter for the enumerator.
    To invoke this function, you will need to specify what type `α` is,
    for example by doing `runEnum (α := Nat) 10`. -/
def runEnum [Enum α] (size : Nat) (limit : Nat := 10) : IO (List α) :=
  return (LazyList.take limit $ Enum.enum size)

/-- Samples from an `ExceptT GenError Enumerator` enumerator that is parameterized by its `size`,
    returning the enumerated list of `Except GenError α` values (containing up to `limit` elements) in the `IO` monad -/
def runSizedEnum (sizedEnum : Nat → ExceptT GenError Enumerator α) (size : Nat) (limit : Nat := 10) : IO (List (Except GenError α)) :=
  return (LazyList.take limit $ (sizedEnum size) size)

/-- Like `runSizedEnum`, but filters out errors and pairs each successful value with the
    accumulated error count seen so far. A nonzero error count means the enumeration was
    incomplete at this size — try a larger size for more results. -/
def runSizedEnumOk (sizedEnum : Nat → ExceptT GenError Enumerator α) (size : Nat) : LazyList (α × Nat) :=
  let raw := (sizedEnum size) size
  let rec go (l : LazyList (Except GenError α)) (errCount : Nat) : LazyList (α × Nat) :=
    match l with
    | .lnil => .lnil
    | .lcons x xs =>
      match x with
      | .ok v => .lcons (v, errCount) (Thunk.mk fun _ => go xs.get errCount)
      | .error _ => go xs.get (errCount + 1)
  go raw 0
