///|
fn[T : Compare] smaller(a : T, b : T) -> T {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn[T : Compare] larger(a : T, b : T) -> T {
  if a > b {
    a
  } else {
    b
  }
}

///|
/// All rates, bursts, times and capacities use caller-selected consistent units.
pub suberror CalculusError {
  Invalid(String)
  Unbounded(String)
  Infeasible(String)
  Limit(String)
  Numerical(String)
} derive(@debug.Debug)

///|
pub(all) struct TokenBucket {
  burst : Double
  rate : Double
} derive(@debug.Debug, ToJson, Eq)

///|
pub(all) struct RateLatency {
  capacity : Double
  latency : Double
} derive(@debug.Debug, ToJson, Eq)

///|
pub(all) struct AnalyticBounds {
  backlog : Double
  delay : Double
  output : TokenBucket
  utilization : Double
} derive(@debug.Debug, ToJson)

///|
fn finite(x : Double, name : String) -> Unit raise CalculusError {
  if x.is_nan() || x.is_inf() {
    raise Numerical("\{name} must be finite")
  }
}

///|
fn nonnegative(x : Double, name : String) -> Unit raise CalculusError {
  finite(x, name)
  if x < 0.0 {
    raise Invalid("\{name} must be nonnegative")
  }
}

///|
fn positive(x : Double, name : String) -> Unit raise CalculusError {
  finite(x, name)
  if x <= 0.0 {
    raise Invalid("\{name} must be positive")
  }
}

///|
fn checked(x : Double, name : String) -> Double raise CalculusError {
  finite(x, name)
  x
}

///|
fn validate_arrival(a : TokenBucket) -> Unit raise CalculusError {
  nonnegative(a.burst, "burst")
  nonnegative(a.rate, "rate")
}

///|
fn validate_service(s : RateLatency) -> Unit raise CalculusError {
  positive(s.capacity, "capacity")
  nonnegative(s.latency, "latency")
}

///|
pub fn token_bucket(
  burst : Double,
  rate : Double,
) -> TokenBucket raise CalculusError {
  let a = TokenBucket::{ burst, rate, }
  validate_arrival(a)
  a
}

///|
pub fn rate_latency(
  capacity : Double,
  latency : Double,
) -> RateLatency raise CalculusError {
  let s = RateLatency::{ capacity, latency, }
  validate_service(s)
  s
}

///|
/// α(0)=0; α(t)=b+rt for t>0, including its right-hand jump b.
pub fn TokenBucket::at(
  self : TokenBucket,
  t : Double,
) -> Double raise CalculusError {
  validate_arrival(self)
  nonnegative(t, "time")
  if t == 0.0 {
    0.0
  } else {
    checked(self.burst + self.rate * t, "arrival value")
  }
}

///|
pub fn RateLatency::at(
  self : RateLatency,
  t : Double,
) -> Double raise CalculusError {
  validate_service(self)
  nonnegative(t, "time")
  checked(self.capacity * larger(0.0, t - self.latency), "service value")
}

///|
/// Infinite-horizon fluid bounds, conditional on an arrival envelope and
/// a deterministic service curve. A delay interpretation requires FIFO.
/// Equality r=R is stable for these deterministic envelopes.
pub fn bounds(
  a : TokenBucket,
  s : RateLatency,
) -> AnalyticBounds raise CalculusError {
  validate_arrival(a)
  validate_service(s)
  if a.rate > s.capacity {
    raise Unbounded("arrival rate exceeds service capacity")
  }
  let burst = checked(a.burst + a.rate * s.latency, "output burst")
  let idle = a.burst == 0.0 && a.rate == 0.0
  {
    backlog: burst,
    delay: if idle {
      0.0
    } else {
      checked(s.latency + a.burst / s.capacity, "delay")
    },
    output: { burst, rate: a.rate, },
    utilization: a.rate / s.capacity,
  }
}

///|
/// Sum of arrival envelopes; no statistical independence is assumed.
pub fn aggregate(
  arrivals : Array[TokenBucket],
) -> TokenBucket raise CalculusError {
  if arrivals.length() > 128 {
    raise Limit("at most 128 envelopes")
  }
  let mut burst = 0.0
  let mut rate = 0.0
  for a in arrivals {
    validate_arrival(a)
    burst = checked(burst + a.burst, "aggregate burst")
    rate = checked(rate + a.rate, "aggregate rate")
  }
  { burst, rate, }
}

///|
/// Convolution of rate-latency service curves: minimum rate, summed latency.
pub fn tandem(services : Array[RateLatency]) -> RateLatency raise CalculusError {
  if services.is_empty() {
    raise Invalid("tandem needs a service")
  }
  if services.length() > 128 {
    raise Limit("at most 128 services")
  }
  let mut capacity = @double.max_value
  let mut latency = 0.0
  for s in services {
    validate_service(s)
    capacity = smaller(capacity, s.capacity)
    latency = checked(latency + s.latency, "tandem latency")
  }
  { capacity, latency, }
}

///|
/// Blind/arbitrary multiplexing leftover. Requires a STRICT server service
/// curve and a valid aggregate envelope for all cross traffic at this node.
pub fn leftover(
  s : RateLatency,
  cross : TokenBucket,
) -> RateLatency raise CalculusError {
  validate_service(s)
  validate_arrival(cross)
  let capacity = s.capacity - cross.rate
  if capacity <= 0.0 {
    raise Unbounded("cross traffic leaves no positive service rate")
  }
  let latency = checked(
    checked(s.capacity * s.latency + cross.burst, "leftover numerator") /
    capacity,
    "leftover latency",
  )
  { capacity, latency, }
}

///|
pub(all) struct CapacityPlan {
  minimum_capacity : Double
  necessary_buffer : Double
  delay_limit : Double
  latency : Double
} derive(@debug.Debug, ToJson)

///|
/// Analytic inverse for a single fluid server. A zero minimum describes idle
/// traffic, not a constructible positive-rate RateLatency server.
pub fn required_capacity(
  a : TokenBucket,
  latency : Double,
  delay_limit : Double,
  buffer_limit : Double,
) -> CapacityPlan raise CalculusError {
  validate_arrival(a)
  nonnegative(latency, "latency")
  nonnegative(delay_limit, "delay limit")
  nonnegative(buffer_limit, "buffer limit")
  let idle = a.burst == 0.0 && a.rate == 0.0
  let necessary_buffer = checked(a.burst + a.rate * latency, "necessary buffer")
  if necessary_buffer > buffer_limit {
    raise Infeasible("latency alone exceeds buffer budget")
  }
  if !idle &&
    (delay_limit < latency || (delay_limit == latency && a.burst > 0.0)) {
    raise Infeasible("no finite rate meets the delay target")
  }
  let minimum_capacity = if idle {
    0.0
  } else if a.burst == 0.0 {
    a.rate
  } else {
    larger(
      a.rate,
      checked(a.burst / (delay_limit - latency), "minimum capacity"),
    )
  }
  { minimum_capacity, necessary_buffer, delay_limit, latency, }
}

///|
pub(all) struct Admission {
  admitted : Bool
  delay_ok : Bool
  buffer_ok : Bool
  metrics : AnalyticBounds
} derive(@debug.Debug, ToJson)

///|
pub fn admit(
  a : TokenBucket,
  s : RateLatency,
  delay_limit : Double,
  buffer_limit : Double,
) -> Admission raise CalculusError {
  nonnegative(delay_limit, "delay limit")
  nonnegative(buffer_limit, "buffer limit")
  let metrics = bounds(a, s)
  let delay_ok = metrics.delay <= delay_limit
  let buffer_ok = metrics.backlog <= buffer_limit
  { admitted: delay_ok && buffer_ok, delay_ok, buffer_ok, metrics, }
}