import Specimen.Enumerators
import Specimen.LazyList
import Plausible.Gen
import Plausible.ArbitraryFueled

open LazyList Plausible

-- Adapted from QuickChick source code
-- https://github.com/QuickChick/QuickChick/blob/master/src/Enumerators.v

namespace EnumeratorCombinators

/-- `pickDrop xs n` and returns the `n`-th enumerator from the list `xs`,
    and returns the tail of the list from the `n+1`-th element onwards
    - Note: this is a variant of `Gen.pickDrop` where the input list does not contain weights
      (enumerators don't have weights attached to them, unlike generators) -/
def pickDrop (xs : List (ExceptT GenError Enumerator α)) (n : Nat) : ExceptT GenError Enumerator α × List (ExceptT GenError Enumerator α) :=
  match xs with
  | [] => (throw <| .genError "EnumeratorCombinators.pickDrop: Empty list", [])
  | x :: xs =>
    match n with
    | .zero => (x, xs)
    | .succ n' =>
      let (x', xs') := pickDrop xs n'
      (x', x::xs')

/-- Helper function for `backtrack` which picks one out of `total` enumerators with some initial amount of `fuel` -/
def enumerateFuel (fuel : Nat) (total : Nat) (es : List (ExceptT GenError Enumerator α)) : ExceptT GenError Enumerator α :=
  match fuel with
  | .zero => throw (.genError "out of fuel")
  | .succ fuel' => do
    let n ← monadLift $ enumNatRange 0 (total - 1)
    let (e, es') := pickDrop es n
    -- Try to enumerate a value using `e`, if it fails, backtrack with `fuel'`
    -- and pick one out of the `total - k` remaining enumerators
    tryCatch e (fun _ => enumerateFuel fuel' (total - 1) es')

/-- Combines all enumerators into a single lazy list. -/
def enumerateAll (es : List (ExceptT GenError Enumerator α)) (fuel : Nat) : LazyList (Except GenError α) :=
  es.foldl (fun acc e => LazyList.append (e fuel) acc) .lnil

/-- Tries all enumerators from a list until one returns a `pure` value or all the enumerators have
    failed once. -/
def enumerate (es : List (ExceptT GenError Enumerator α)) : ExceptT GenError Enumerator α :=
  enumerateAll es

/-- Applies the checker `f` to a `LazyList l` of values, returning the resultant `Except ε Bool`
    (the parameter `anyNone` is used to indicate whether any of the elements examined previously have been `none`) -/
def lazyListBacktrack (l : LazyList α) (f : α → Except GenError Bool) (anyNone : Bool) : Except GenError Bool :=
  let err := GenError.genError "EnumeratorCombinators.lazyListBackTrack: failure"
  match l with
  | .lnil => if anyNone then throw err else .ok false
  | .lcons x xs =>
    match f x with
    | .ok true => .ok true
    | .ok false => lazyListBacktrack xs.get f anyNone
    | .error _ => lazyListBacktrack xs.get f true

/-- Variant of `lazyListBacktrack` where the input `LazyList` contains `Except ε α` values instead of `α` -/
def lazyListBacktrackOpt (l : LazyList (Except GenError α)) (f : α → Except ε Bool) (anyNone : Bool) : Except GenError Bool :=
  let err := GenError.genError "EnumeratorCombinators.lazyListBackTrackOpt: failure"
  match l with
  | .lnil => if anyNone then throw err else .ok false
  | .lcons mx xs =>
    match mx with
    | .ok x =>
      match f x with
      | .ok true => .ok true
      | .ok false => lazyListBacktrackOpt xs.get f anyNone
      | .error _ => lazyListBacktrackOpt xs.get f true
    | .error _ => lazyListBacktrackOpt xs.get f true

/-- Iterates through all the results of the enumerator `e`, applies the checker `f` to them,
    and returns the resultant `Except GenError Bool`. -/
def enumerating (e : Enumerator α) (f : α → Except GenError Bool) (size : Nat) : Except GenError Bool :=
  lazyListBacktrack (e size) f false

/-- Variant of `enumerating`, except the input enumerator `e` may fail and has type `ExceptT GenError Enumerator α`
    - This corresponds to `bind_EC` in the Computing Correctly paper (section 4) -/
def enumeratingOpt (e : ExceptT GenError Enumerator α) (f : α → Except GenError Bool) (size : Nat) : Except GenError Bool :=
  lazyListBacktrackOpt (e size) f false

end EnumeratorCombinators
