///|
/// The step `InvalidRequests` moves its window in.
const INVALID_SLOT_MS : Int64 = 10_000

///|
/// How many steps the window spans: ten minutes, Discord's window for
/// invalid requests.
const INVALID_SLOTS : Int32 = 60

///|
/// A rolling count of the invalid requests one client received, for
/// Discord's limit of 10,000 per ten minutes, past which Discord restricts
/// the address for a while. The window moves in ten-second steps, so a request
/// leaves the count between 590 and 600 seconds after it was answered.
///
/// It is a fixed ring of `INVALID_SLOTS` counters, each labelled with the
/// step it counts; a counter whose step has left the window is reset when
/// the ring comes back to it, so the memory never grows.
priv struct InvalidRequests {
  // The step (time / INVALID_SLOT_MS) each counter counts, or Int64 min for
  // none yet.
  steps : FixedArray[Int64]
  counts : FixedArray[Int32]
}

///|
fn InvalidRequests::new() -> InvalidRequests {
  {
    steps: FixedArray::make(INVALID_SLOTS, -9223372036854775808L),
    counts: FixedArray::make(INVALID_SLOTS, 0),
  }
}

///|
/// The step that `now_unix_ms` falls in, rounding down.
fn invalid_step(now_unix_ms : Int64) -> Int64 {
  let step = now_unix_ms / INVALID_SLOT_MS
  if now_unix_ms < 0L && step * INVALID_SLOT_MS != now_unix_ms {
    step - 1L
  } else {
    step
  }
}

///|
/// The ring index of `step`.
fn invalid_slot(step : Int64) -> Int32 {
  let slot = (step % INVALID_SLOTS.to_int64()).to_int()
  if slot < 0 {
    slot + INVALID_SLOTS
  } else {
    slot
  }
}

///|
/// Counts one invalid request answered at `now_unix_ms`.
fn InvalidRequests::record(self : InvalidRequests, now_unix_ms : Int64) -> Unit {
  let step = invalid_step(now_unix_ms)
  let slot = invalid_slot(step)
  if self.steps[slot] != step {
    self.steps[slot] = step
    self.counts[slot] = 0
  }
  self.counts[slot] += 1
}

///|
/// The invalid requests answered in the window that ends at `now_unix_ms`.
fn InvalidRequests::count(self : InvalidRequests, now_unix_ms : Int64) -> Int32 {
  let step = invalid_step(now_unix_ms)
  let oldest = step - INVALID_SLOTS.to_int64()
  let mut total = 0
  for slot in 0.. oldest && counted <= step {
      total += self.counts[slot]
    }
  }
  total
}

///|
/// Whether Discord counts a response with `status` and `headers` as an
/// invalid request: a 401, a 403, or a 429 other than one with
/// `X-RateLimit-Scope: shared`, which Discord says it does not count.
fn is_invalid_request(status : Int32, headers : @ghttp.Headers) -> Bool {
  match status {
    401 | 403 => true
    429 =>
      headers.get("x-ratelimit-scope").map(value => value.trim().to_lower()) !=
      Some("shared")
    _ => false
  }
}