// HPACK header compression: the dynamic table with size-bounded eviction
// (RFC 7541 §2.3.2, §4), the six header-field representations (§6.1–6.3), and a
// stateful encoder/decoder pair that share the static (§2.3.1) and dynamic index
// address spaces. Byte-oriented throughout so binary metadata (`-bin`) survives.

///|
/// A decoded header field: `name` and `value` as raw octet strings (HTTP/2 header
/// names and values are byte sequences, and gRPC `-bin` metadata is binary).
pub(all) struct Header {
  name : Bytes
  value : Bytes
} derive(Eq, Debug)

///|
/// ASCII/latin-1 `String` → `Bytes`, one octet per code unit. Used only for the
/// static table, whose entries are all ASCII (RFC 7541 Appendix A).
fn ascii_to_bytes(s : String) -> Bytes {
  let buf = Buffer()
  for i = 0; i < s.length(); i = i + 1 {
    buf.write_byte((s[i].to_int() & 0xFF).to_byte())
  }
  buf.to_bytes()
}

///|
/// The static table as `(name, value)` octet pairs, in RFC index order (index 1
/// at position 0). Derived once from `static_table`.
let static_table_bytes : Array[(Bytes, Bytes)] = {
  let out : Array[(Bytes, Bytes)] = []
  for e in static_table {
    out.push((ascii_to_bytes(e.0), ascii_to_bytes(e.1)))
  }
  out
}

// -- string literals with Huffman (RFC 7541 §5.2) ---------------------------

///|
/// Encode `octets` as a Huffman-coded HPACK string literal (RFC 7541 §5.2): the
/// `H` bit set, the Huffman length as a 7-bit-prefix integer, then the code.
pub fn hpack_encode_string_huffman(octets : Bytes) -> Bytes {
  let coded = huffman_encode(octets)
  let lp = hpack_encode_int(coded.length(), 7)
  let buf = Buffer()
  buf.write_byte((lp[0].to_int() | 0x80).to_byte())
  buf.write_bytes(lp[1:].to_owned())
  buf.write_bytes(coded)
  buf.to_bytes()
}

///|
/// Encode `octets` as an HPACK string literal, choosing the shorter of the raw
/// (`H = 0`) and Huffman (`H = 1`) forms — the standard encoder heuristic.
pub fn hpack_encode_string_auto(octets : Bytes) -> Bytes {
  if huffman_encoded_length(octets) < octets.length() {
    hpack_encode_string_huffman(octets)
  } else {
    hpack_encode_string(octets)
  }
}

///|
/// Read an HPACK string literal at `offset`, resolving Huffman coding when the
/// `H` bit is set, returning `(octets, bytes_consumed)`. Unlike
/// `hpack_decode_string`, this applies Huffman decoding. Raises on bad Huffman.
pub fn hpack_read_string(
  data : Bytes,
  offset : Int,
) -> (Bytes, Int) raise HpackError {
  let huff = hpack_string_is_huffman(data, offset)
  let (raw, consumed) = hpack_decode_string(data, offset)
  if huff {
    (huffman_decode(raw), consumed)
  } else {
    (raw, consumed)
  }
}

// -- dynamic table (RFC 7541 §2.3.2, §4) ------------------------------------

///|
/// The HPACK dynamic table: a FIFO of recently seen `(name, value)` entries,
/// newest first (`entries[0]`), bounded by `max_size` octets where each entry
/// costs `name.len + value.len + 32` (RFC 7541 §4.1). Adding evicts the oldest
/// entries until the newcomer fits; an entry larger than `max_size` empties the
/// table and is not stored (§4.4).
pub(all) struct DynamicTable {
  mut entries : Array[(Bytes, Bytes)]
  mut size : Int
  mut max_size : Int
}

///|
/// A new empty dynamic table bounded by `max_size` octets (default 4096, the
/// HTTP/2 initial `SETTINGS_HEADER_TABLE_SIZE`).
pub fn DynamicTable::new(max_size? : Int = 4096) -> DynamicTable {
  { entries: [], size: 0, max_size }
}

///|
fn entry_size(name : Bytes, value : Bytes) -> Int {
  name.length() + value.length() + 32
}

///|
/// Evict oldest entries until the table is within `max_size`.
fn DynamicTable::evict_to_fit(self : DynamicTable) -> Unit {
  while self.size > self.max_size && self.entries.length() > 0 {
    let old = self.entries.remove(self.entries.length() - 1)
    self.size = self.size - entry_size(old.0, old.1)
  }
}

///|
/// Resize the table (a dynamic table size update, RFC 7541 §4.2), evicting to fit.
pub fn DynamicTable::set_max_size(self : DynamicTable, new_max : Int) -> Unit {
  self.max_size = new_max
  self.evict_to_fit()
}

///|
/// Insert `(name, value)` at the front, evicting oldest entries to make room. If
/// the entry alone exceeds `max_size` the table ends up empty (RFC 7541 §4.4).
pub fn DynamicTable::add(
  self : DynamicTable,
  name : Bytes,
  value : Bytes,
) -> Unit {
  let sz = entry_size(name, value)
  while self.size + sz > self.max_size && self.entries.length() > 0 {
    let old = self.entries.remove(self.entries.length() - 1)
    self.size = self.size - entry_size(old.0, old.1)
  }
  if sz <= self.max_size {
    self.entries.insert(0, (name, value))
    self.size = self.size + sz
  }
}

///|
/// The number of entries currently in the dynamic table.
pub fn DynamicTable::count(self : DynamicTable) -> Int {
  self.entries.length()
}

///|
/// The current total size of the dynamic table in octets (§4.1 accounting).
pub fn DynamicTable::current_size(self : DynamicTable) -> Int {
  self.size
}

///|
/// Resolve an HPACK index against the combined static + dynamic address space
/// (RFC 7541 §2.3.3): `1..=61` index the static table; `62..` index the dynamic
/// table newest-first. Returns `None` when out of range.
fn hpack_lookup(dt : DynamicTable, index : Int) -> (Bytes, Bytes)? {
  let statics = static_table_bytes.length()
  if index >= 1 && index <= statics {
    Some(static_table_bytes[index - 1])
  } else {
    let d = index - statics - 1
    if d >= 0 && d < dt.entries.length() {
      Some(dt.entries[d])
    } else {
      None
    }
  }
}

///|
/// Encode a dynamic table size update (RFC 7541 §6.3): `001` prefix with the new
/// maximum size as a 5-bit-prefix integer.
pub fn hpack_encode_size_update(new_max : Int) -> Bytes {
  let e = hpack_encode_int(new_max, 5)
  let buf = Buffer()
  buf.write_byte((e[0].to_int() | 0x20).to_byte())
  buf.write_bytes(e[1:].to_owned())
  buf.to_bytes()
}

// -- decoder (RFC 7541 §6) --------------------------------------------------

///|
/// A stateful HPACK decoder: it owns a dynamic table that persists across the
/// header blocks of a connection. `limit` is the peer-agreed hard cap
/// (`SETTINGS_HEADER_TABLE_SIZE`) a size update may not exceed.
pub(all) struct HpackDecoder {
  table : DynamicTable
  limit : Int
}

///|
/// A new decoder whose dynamic table is bounded by `max_size` octets (also the
/// hard cap enforced on dynamic table size updates).
pub fn HpackDecoder::new(max_size? : Int = 4096) -> HpackDecoder {
  { table: DynamicTable::new(max_size~), limit: max_size }
}

///|
/// Read a header-field name that is either indexed (`index != 0`) or a following
/// string literal (`index == 0`).
fn HpackDecoder::read_name(
  self : HpackDecoder,
  block : Bytes,
  index : Int,
  off : Int,
) -> (Bytes, Int) raise HpackError {
  if index == 0 {
    hpack_read_string(block, off)
  } else {
    match hpack_lookup(self.table, index) {
      Some((name, _)) => (name, 0)
      None =>
        raise HpackDecodeError("name index out of range: " + index.to_string())
    }
  }
}

///|
/// Decode one complete header block into its header list (RFC 7541 §6), mutating
/// the dynamic table for incrementally indexed fields and size updates. Raises
/// `HpackDecodeError`/`HuffmanError` on any malformed representation.
pub fn HpackDecoder::decode(
  self : HpackDecoder,
  block : Bytes,
) -> Array[Header] raise HpackError {
  let out : Array[Header] = []
  let mut off = 0
  let n = block.length()
  while off < n {
    let b = block[off].to_int()
    if (b & 0x80) != 0 {
      // §6.1 Indexed Header Field.
      let (index, consumed) = hpack_decode_int(block, off, 7)
      off = off + consumed
      if index == 0 {
        raise HpackDecodeError("indexed header field with index 0")
      }
      match hpack_lookup(self.table, index) {
        Some((name, value)) => out.push({ name, value })
        None =>
          raise HpackDecodeError("index out of range: " + index.to_string())
      }
    } else if (b & 0x40) != 0 {
      // §6.2.1 Literal Header Field with Incremental Indexing.
      let (index, consumed) = hpack_decode_int(block, off, 6)
      off = off + consumed
      let (name, name_consumed) = self.read_name(block, index, off)
      off = off + name_consumed
      let (value, value_consumed) = hpack_read_string(block, off)
      off = off + value_consumed
      self.table.add(name, value)
      out.push({ name, value })
    } else if (b & 0x20) != 0 {
      // §6.3 Dynamic Table Size Update.
      let (new_max, consumed) = hpack_decode_int(block, off, 5)
      off = off + consumed
      if new_max > self.limit {
        raise HpackDecodeError(
          "dynamic table size update exceeds SETTINGS limit",
        )
      }
      self.table.set_max_size(new_max)
    } else {
      // §6.2.2 without indexing (0x00) / §6.2.3 never indexed (0x10); 4-bit prefix.
      let (index, consumed) = hpack_decode_int(block, off, 4)
      off = off + consumed
      let (name, name_consumed) = self.read_name(block, index, off)
      off = off + name_consumed
      let (value, value_consumed) = hpack_read_string(block, off)
      off = off + value_consumed
      out.push({ name, value })
    }
  }
  out
}

// -- encoder (RFC 7541 §6) --------------------------------------------------

///|
/// A stateful HPACK encoder: it owns a dynamic table mirroring the decoder's, and
/// prefers indexed representations. `huffman` selects Huffman string literals when
/// they are shorter.
pub(all) struct HpackEncoder {
  table : DynamicTable
  mut huffman : Bool
}

///|
/// A new encoder bounded by `max_size` octets; `huffman` (default `true`) enables
/// the shorter-of-two string-literal heuristic.
pub fn HpackEncoder::new(
  max_size? : Int = 4096,
  huffman? : Bool = true,
) -> HpackEncoder {
  { table: DynamicTable::new(max_size~), huffman }
}

///|
/// Find the best index for `(name, value)`: an exact `(idx, true)` match, else a
/// name-only `(idx, false)` match, else `(0, false)` for no match at all.
fn HpackEncoder::find(
  self : HpackEncoder,
  name : Bytes,
  value : Bytes,
) -> (Int, Bool) {
  let statics = static_table_bytes.length()
  let mut name_idx = 0
  for i = 0; i < statics; i = i + 1 {
    let (n, v) = static_table_bytes[i]
    if n == name {
      if v == value {
        return (i + 1, true)
      }
      if name_idx == 0 {
        name_idx = i + 1
      }
    }
  }
  for d = 0; d < self.table.entries.length(); d = d + 1 {
    let (n, v) = self.table.entries[d]
    if n == name {
      let idx = statics + 1 + d
      if v == value {
        return (idx, true)
      }
      if name_idx == 0 {
        name_idx = idx
      }
    }
  }
  (name_idx, false)
}

///|
fn HpackEncoder::encode_string(self : HpackEncoder, octets : Bytes) -> Bytes {
  if self.huffman {
    hpack_encode_string_auto(octets)
  } else {
    hpack_encode_string(octets)
  }
}

///|
/// Write an integer with a flag OR'd into the high bits of its first octet.
fn write_prefixed(
  buf : Buffer,
  value : Int,
  prefix_bits : Int,
  flag : Int,
) -> Unit {
  let e = hpack_encode_int(value, prefix_bits)
  buf.write_byte((e[0].to_int() | flag).to_byte())
  buf.write_bytes(e[1:].to_owned())
}

///|
/// Encode a header list into a header block (RFC 7541 §6), using indexed fields
/// where possible and literal-with-incremental-indexing otherwise (mutating the
/// dynamic table to mirror what the peer decoder will build). The output decodes
/// back to the same header list via `HpackDecoder`.
pub fn HpackEncoder::encode(
  self : HpackEncoder,
  headers : Array[Header],
) -> Bytes {
  let buf = Buffer()
  for h in headers {
    let (idx, exact) = self.find(h.name, h.value)
    if exact {
      write_prefixed(buf, idx, 7, 0x80)
    } else {
      if idx != 0 {
        write_prefixed(buf, idx, 6, 0x40)
      } else {
        buf.write_byte(b'\x40')
        buf.write_bytes(self.encode_string(h.name))
      }
      buf.write_bytes(self.encode_string(h.value))
      self.table.add(h.name, h.value)
    }
  }
  buf.to_bytes()
}