///|
/// Sliding-window log limiter for gateway commands: Discord allows 120 sends
/// per 60 seconds per connection; a few slots are reserved for heartbeats
/// (which bypass this limiter), so user commands default to 115.
pub struct CommandLimiter {
  priv timestamps : Array[Int64]
  priv limit : Int32
  priv window_ms : Int64
  priv clock_ : &@hclock.Clock
}

///|
/// Create a limiter allowing `limit` sends per sliding window (60 seconds by default).
pub fn CommandLimiter::CommandLimiter(
  limit~ : Int32 = 115,
  window_ms~ : Int32 = 60000,
  clock~ : &@hclock.Clock = @transport.AsyncClock::new(),
) -> CommandLimiter {
  { timestamps: [], limit, window_ms: window_ms.to_int64(), clock_: clock, }
}

///|
/// Wait until a send slot is available within the window, then consume it.
pub async fn CommandLimiter::acquire(self : CommandLimiter) -> Unit {
  self.wait_for_slot()
  self.record()
}

///|
/// Drop timestamps that left the window at `now`, then return how long to wait
/// for a free slot: `None` when one is free, otherwise until the oldest send
/// leaves the window, plus a 1 ms nudge.
fn CommandLimiter::slot_wait_ms(self : CommandLimiter, now : Int64) -> Int32? {
  while self.timestamps.length() > 0 &&
        now - self.timestamps[0] >= self.window_ms {
    self.timestamps.remove(0) |> ignore
  }
  if self.timestamps.length() < self.limit {
    None
  } else {
    Some((self.timestamps[0] + self.window_ms - now).to_int() + 1)
  }
}

///|
// Callers must serialize waiting, sending, and recording to avoid oversubscription.
async fn CommandLimiter::wait_for_slot(self : CommandLimiter) -> Unit {
  while self.slot_wait_ms(self.clock_.now_unix_ms()) is Some(ms) {
    self.clock_.sleep(ms)
  }
}

///|
fn CommandLimiter::record(self : CommandLimiter) -> Unit {
  self.timestamps.push(self.clock_.now_unix_ms())
}