///|
pub fn budget(
  max_calls? : Int,
  max_bytes? : Int64,
  expires_at? : Int64,
) -> Budget raise PermitError {
  match max_calls {
    Some(limit) => if limit <= 0 { raise PermitError::InvalidCallLimit(limit) }
    None => ()
  }
  match max_bytes {
    Some(limit) => if limit < 0L { raise PermitError::InvalidByteLimit(limit) }
    None => ()
  }
  match expires_at {
    Some(expiry) => if expiry < 0L { raise PermitError::InvalidExpiry(expiry) }
    None => ()
  }
  { max_calls, max_bytes, expires_at, }
}

///|
pub fn unlimited_budget() -> Budget {
  { max_calls: None, max_bytes: None, expires_at: None, }
}

///|
pub fn Budget::max_calls(self : Budget) -> Int? {
  self.max_calls
}

///|
pub fn Budget::max_bytes(self : Budget) -> Int64? {
  self.max_bytes
}

///|
pub fn Budget::expires_at(self : Budget) -> Int64? {
  self.expires_at
}

///|
fn optional_int_contains(grant : Int?, requested : Int?) -> Bool {
  match (grant, requested) {
    (None, _) => true
    (Some(_), None) => false
    (Some(grant), Some(requested)) => requested <= grant
  }
}

///|
fn optional_int64_contains(grant : Int64?, requested : Int64?) -> Bool {
  match (grant, requested) {
    (None, _) => true
    (Some(_), None) => false
    (Some(grant), Some(requested)) => requested <= grant
  }
}

///|
pub fn budget_contains(grant : Budget, requested : Budget) -> Bool {
  optional_int_contains(grant.max_calls, requested.max_calls) &&
  optional_int64_contains(grant.max_bytes, requested.max_bytes) &&
  optional_int64_contains(grant.expires_at, requested.expires_at)
}

///|
fn union_optional_int(left : Int?, right : Int?) -> Int? {
  match (left, right) {
    (None, _) | (_, None) => None
    (Some(left), Some(right)) => Some(if left >= right { left } else { right })
  }
}

///|
fn union_optional_int64(left : Int64?, right : Int64?) -> Int64? {
  match (left, right) {
    (None, _) | (_, None) => None
    (Some(left), Some(right)) => Some(if left >= right { left } else { right })
  }
}

///|
fn budget_union(left : Budget, right : Budget) -> Budget {
  {
    max_calls: union_optional_int(left.max_calls, right.max_calls),
    max_bytes: union_optional_int64(left.max_bytes, right.max_bytes),
    expires_at: union_optional_int64(left.expires_at, right.expires_at),
  }
}

///|
fn optional_int_text(value : Int?) -> String {
  match value {
    Some(value) => "\{value}"
    None => "*"
  }
}

///|
fn optional_int64_text(value : Int64?) -> String {
  match value {
    Some(value) => "\{value}"
    None => "*"
  }
}

///|
fn Budget::canonical(self : Budget) -> String {
  "calls=" +
  optional_int_text(self.max_calls) +
  ";bytes=" +
  optional_int64_text(self.max_bytes) +
  ";expires=" +
  optional_int64_text(self.expires_at)
}