// Fuel: the recursion budget of the parser and of every other recursive
// operation of the library, as the Scala 3.10 compiler counts it. One
// unit per nested entry of a fuel-checked operation; the budget is a
// value, so leaving a scope gives the outer budget back by construction.

///|
/// Units of fuel, at least 1.
///
/// ```mbt check
/// test {
///   debug_inspect(@fuel.Budget::new(100).map(b => b.units()), content="Some(100)")
///   debug_inspect(@fuel.Budget::new(0), content="None")
/// }
/// ```
pub struct Budget(Int32) derive(Eq, Compare, Debug)

///|
/// A budget of `units`; `None` below 1.
pub fn Budget::new(units : Int32) -> Budget? {
  if units >= 1 {
    Some(Budget(units))
  } else {
    None
  }
}

///|
/// The number of units.
pub fn Budget::units(self : Budget) -> Int32 {
  self.0
}

///|
/// The default budget, 100 units: measured safe on wasm, the smallest
/// stack (it overflows at about 140 nested lists). Raise it only on
/// native or js.
pub let default_budget : Budget = Budget(100)

///|
/// The name of a fuel-checked operation (`expression`, `pattern`, ...).
/// Each package that spends fuel defines its operations as constants.
pub struct Operation(String) derive(Eq, Hash, Debug)

///|
/// An operation with this name; the name appears in `FuelExhausted`.
pub fn Operation::new(name : String) -> Operation {
  Operation(name)
}

///|
/// The operation's name.
pub fn Operation::name(self : Operation) -> String {
  self.0
}

///|
/// One operation on the stack with the number of nested levels it holds.
pub(all) struct Spent {
  operation : Operation
  levels : Int32
} derive(Eq, Debug)

///|
/// An immutable fuel value: the budget, the units not yet spent, and the
/// operations on the stack (innermost first). `spend` gives a new value;
/// there is no restore: the enclosing operation keeps its own `Fuel`.
///
/// ```mbt check
/// test {
///   let f = @fuel.Fuel::new(budget=@fuel.Budget::new(2).unwrap())
///   let inner = f.spend(@fuel.Operation::new("expression"))
///   inspect(inner.left(), content="1")
///   inspect(f.left(), content="2")
/// }
/// ```
pub struct Fuel {
  budget : Budget
  left : Int32
  ops : @list.List[Operation]
} derive(Eq, Debug)

///|
/// A full budget (`default_budget` when none is given).
pub fn Fuel::new(budget~ : Budget = default_budget) -> Fuel {
  { budget, left: budget.units(), ops: @list.List([]), }
}

///|
/// Enter a nested operation: one unit less, `op` on the stack. Raises
/// `FuelExhausted` with the budget and the stack when no unit is left.
pub fn Fuel::spend(self : Fuel, op : Operation) -> Fuel raise FuelExhausted {
  if self.left == 0 {
    raise FuelExhausted(budget=self.budget, spent=self.spent())
  }
  { budget: self.budget, left: self.left - 1, ops: self.ops.prepend(op), }
}

///|
/// The units not yet spent.
pub fn Fuel::left(self : Fuel) -> Int32 {
  self.left
}

///|
/// The budget this fuel started with.
pub fn Fuel::budget(self : Fuel) -> Budget {
  self.budget
}

///|
/// The distinct operations on the stack, outermost first, each with the
/// number of levels it holds.
pub fn Fuel::spent(self : Fuel) -> Array[Spent] {
  let out : Array[Spent] = []
  for op in self.ops.rev() {
    let mut found = false
    for i, s in out {
      if s.operation == op {
        out[i] = { operation: op, levels: s.levels + 1, }
        found = true
        break
      }
    }
    if !found {
      out.push({ operation: op, levels: 1, })
    }
  }
  out
}

///|
/// The budget ran out: `spent` is the stack at that moment, outermost
/// first.
pub(all) suberror FuelExhausted {
  FuelExhausted(budget~ : Budget, spent~ : Array[Spent])
} derive(Eq, Debug)