///|
/// Serializes shard Identify calls per Discord's `max_concurrency` buckets.
/// One queue instance must be shared by every shard of a bot; cross-process
/// deployments can substitute their own implementation.
pub(open) trait IdentifyQueue {
  /// Block (asynchronously) until this shard may send Identify.
  async fn wait_for_identify(Self, shard_id : Int32) -> Unit
}

///|
priv struct IdentifyBucket {
  gate : @async.Semaphore
  // None until the bucket's first identify, so the first one never waits
  // whatever time the clock starts at.
  mut last_identify_ms : Int64?
}

///|
/// In-process identify queue: `shard_id % max_concurrency` buckets, at most
/// one Identify per bucket per `spacing_ms` (default 5 seconds plus margin).
pub struct InMemoryQueue {
  priv buckets : Array[IdentifyBucket]
  priv spacing_ms : Int32
  priv clock : &@hclock.Clock
}

///|
/// Create a queue with `max_concurrency` identify buckets (the value Discord
/// reports in `GET /gateway/bot`), spacing identifies within each bucket by
/// `spacing_ms`.
pub fn InMemoryQueue::InMemoryQueue(
  max_concurrency~ : Int32 = 1,
  spacing_ms~ : Int32 = 5250,
  clock~ : &@hclock.Clock = @transport.AsyncClock::new(),
) -> InMemoryQueue {
  let buckets = []
  for _ in 0.. Int32 {
  guard last_identify_ms is Some(last) else { return 0 }
  let wait = last + spacing_ms.to_int64() - now
  if wait > 0 {
    wait.to_int()
  } else {
    0
  }
}

///|
/// Wait until the shard's bucket (`shard_id % max_concurrency`) allows the
/// next identify.
pub impl IdentifyQueue for InMemoryQueue with fn wait_for_identify(
  self,
  shard_id,
) {
  let bucket = self.buckets[shard_id % self.buckets.length()]
  bucket.gate.acquire()
  defer bucket.gate.release()
  let wait = identify_wait_ms(
    bucket.last_identify_ms,
    self.spacing_ms,
    self.clock.now_unix_ms(),
  )
  if wait > 0 {
    self.clock.sleep(wait)
  }
  bucket.last_identify_ms = Some(self.clock.now_unix_ms())
}