// The QPACK dynamic table (RFC 9204 §3.2): a FIFO of name/value entries that the encoder-
// stream instructions drive. Each entry costs `name.length() + value.length() + 32` toward
// the table size (§3.2.1); inserting evicts the oldest entries until the newcomer fits, or
// fails if it can never fit (§3.2.2). Entries keep a stable absolute index across evictions
// (§3.2.4); the encoder stream addresses them by a relative index counted back from the most
// recent insertion (§3.2.5). Applying an instruction resolves its name — from the static
// table, from a dynamic entry, or a literal — and inserts. This is the table the field-line
// decoder will read once dynamic references are enabled.

///|
/// A QPACK dynamic table: the live entries, the bytes they occupy, the capacity, the count
/// of entries ever inserted (the next absolute index), and how many have been evicted.
pub(all) struct QpackDynamicTable {
  entries : Array[(Bytes, Bytes)]
  mut used : Int
  mut capacity : Int
  mut insert_count : Int
  mut dropped : Int
}

///|
/// A fresh, empty dynamic table with the given capacity in bytes.
pub fn QpackDynamicTable::new(capacity : Int) -> QpackDynamicTable {
  { entries: [], used: 0, capacity, insert_count: 0, dropped: 0, }
}

///|
/// The size an entry occupies in the table: its name and value octets plus 32 (RFC 9204
/// §3.2.1).
pub fn qpack_entry_size(name : Bytes, value : Bytes) -> Int {
  name.length() + value.length() + 32
}

///|
/// Evict oldest entries until `incoming` more bytes would fit under the capacity, or the
/// table is empty (RFC 9204 §3.2.2).
fn QpackDynamicTable::evict_to_fit(
  self : QpackDynamicTable,
  incoming : Int,
) -> Unit {
  while self.used + incoming > self.capacity && self.entries.length() > 0 {
    let (n, v) = self.entries[0]
    self.used = self.used - qpack_entry_size(n, v)
    let _ = self.entries.remove(0)
    self.dropped = self.dropped + 1
  }
}

///|
/// Set the table capacity, evicting whatever no longer fits (RFC 9204 §3.2.3).
pub fn QpackDynamicTable::set_capacity(
  self : QpackDynamicTable,
  capacity : Int,
) -> Unit {
  self.capacity = capacity
  self.evict_to_fit(0)
}

///|
/// Insert an entry, evicting as needed. Returns `false` (adding nothing) when the entry
/// cannot fit even in an empty table (RFC 9204 §3.2.2).
pub fn QpackDynamicTable::insert(
  self : QpackDynamicTable,
  name : Bytes,
  value : Bytes,
) -> Bool {
  let size = qpack_entry_size(name, value)
  self.evict_to_fit(size)
  if self.used + size > self.capacity {
    return false
  }
  self.entries.push((name, value))
  self.used = self.used + size
  self.insert_count = self.insert_count + 1
  true
}

///|
/// The entry at absolute index `abs_index`, or `None` if it has been evicted or never
/// existed (RFC 9204 §3.2.4).
pub fn QpackDynamicTable::get(
  self : QpackDynamicTable,
  abs_index : Int,
) -> (Bytes, Bytes)? {
  let pos = abs_index - self.dropped
  if pos < 0 || pos >= self.entries.length() {
    None
  } else {
    Some(self.entries[pos])
  }
}

///|
/// The entry at encoder-stream relative index `rel` (0 = most recently inserted, RFC 9204
/// §3.2.5).
pub fn QpackDynamicTable::get_relative(
  self : QpackDynamicTable,
  rel : Int,
) -> (Bytes, Bytes)? {
  self.get(self.insert_count - 1 - rel)
}

///|
/// Apply an encoder-stream instruction to the table (RFC 9204 §4.3): set the capacity, or
/// insert an entry whose name comes from the static table, a dynamic entry (by relative
/// index), or a literal. Returns whether the resulting insert fit (always `true` for a
/// capacity change). Raises if a referenced name index is out of range.
pub fn QpackDynamicTable::apply(
  self : QpackDynamicTable,
  inst : QpackEncoderInst,
) -> Bool raise QpackError {
  match inst {
    SetCapacity(capacity) => {
      self.set_capacity(capacity)
      true
    }
    InsertLiteralName(name~, value~) => self.insert(name, value)
    InsertNameRef(is_static~, index~, value~) => {
      let name = if is_static {
        match qpack_static_get(index) {
          Some((n, _)) => @utf8.encode(n)
          None =>
            raise QpackError(
              "static name index out of range: " + index.to_string(),
            )
        }
      } else {
        match self.get_relative(index) {
          Some((n, _)) => n
          None =>
            raise QpackError(
              "dynamic name index out of range: " + index.to_string(),
            )
        }
      }
      self.insert(name, value)
    }
    Duplicate(index~) =>
      match self.get_relative(index) {
        Some((n, v)) => self.insert(n, v)
        None =>
          raise QpackError("duplicate index out of range: " + index.to_string())
      }
  }
}