///|
priv struct Budget {
  limits : Limits
  mut used : Int32
  mut complete : Bool
  findings : Array[Finding]
}

///|
fn Budget::new(limits : Limits, used~ : Int32 = 0) -> Budget {
  { limits, used, complete: true, findings: [], }
}

///|
/// A request-scoped work allowance. Several phases of one request, such as parsing, projection and
/// validation, charge the same meter, so a later phase receives only the remaining work.
pub struct WorkMeter {
  priv limit : Int32
  priv mut used : Int32
  priv mut exhausted : Bool
}

///|
pub fn WorkMeter::new(limit : Int32) -> WorkMeter raise ConfigurationError {
  guard limit > 0 && limit <= 10000000 else {
    raise ConfigurationError("I expected a work limit of 1..10000000.")
  }
  { limit, used: 0, exhausted: false, }
}

///|
pub fn WorkMeter::limit(self : WorkMeter) -> Int32 {
  self.limit
}

///|
pub fn WorkMeter::used(self : WorkMeter) -> Int32 {
  self.used
}

///|
/// True after a charge was refused. An exhausted meter refuses every later charge.
pub fn WorkMeter::exhausted(self : WorkMeter) -> Bool {
  self.exhausted
}

///|
/// Charge `units` of work. Returns false, and charges nothing, when the meter has too little left.
pub fn WorkMeter::charge(self : WorkMeter, units : Int32) -> Bool {
  if self.exhausted || units < 0 || units > self.limit - self.used {
    self.exhausted = true
    return false
  }
  self.used += units
  true
}

///|
fn Budget::stop(
  self : Budget,
  path : InstancePath,
  shape : @smithy.ShapeId,
  reason : String,
) -> Unit {
  if !self.complete {
    return
  }
  self.complete = false
  let finding : Finding = {
    code: LimitExceeded,
    severity: Error,
    path,
    shape,
    message: "I reached the " +
    reason +
    " validation limit before completing the value.",
  }
  if self.findings.length() < self.limits.findings {
    self.findings.push(finding)
  }
}

///|
fn Budget::charge(
  self : Budget,
  path : InstancePath,
  shape : @smithy.ShapeId,
  units~ : Int32 = 1,
) -> Bool {
  if !self.complete {
    return false
  }
  if units > self.limits.work - self.used {
    self.stop(path, shape, "work")
    return false
  }
  self.used += units
  true
}

///|
fn Budget::room(
  self : Budget,
  path : InstancePath,
  shape : @smithy.ShapeId,
  children : Int32,
  pending~ : Int32 = 0,
) -> Bool {
  if pending > self.limits.work - self.used ||
    children > self.limits.work - self.used - pending {
    self.stop(path, shape, "work")
    false
  } else {
    self.complete
  }
}

///|
fn Budget::emit(
  self : Budget,
  code : FindingCode,
  path : InstancePath,
  shape : @smithy.ShapeId,
  message : String,
) -> Unit {
  if !self.complete {
    return
  }
  if self.findings.length() >= self.limits.findings - 1 {
    self.stop(path, shape, "findings")
    return
  }
  self.findings.push({ code, severity: Error, path, shape, message, })
}