// QPACK field-line encoding (RFC 9204 §4.5): a field section is an encoded-field-
// section prefix followed by a run of field lines, each a header rendered as an indexed
// reference into the static table, a literal value with a static name reference, or a
// fully literal name and value. Strings carry an H bit selecting Huffman coding (RFC
// 7541 Appendix B, the same table HPACK uses — reused from moonrpc). This encoder never
// uses the dynamic table, so it emits a zero insert-count prefix and only static/literal
// representations, which is a complete and interoperable QPACK encoder (RFC 9204 §2.1
// permits an encoder to never index dynamically). The decoder reads those back; a field
// line that references the dynamic table is reported as unsupported (that layer is
// built separately).

///|
/// A malformed or unsupported QPACK field section.
pub suberror QpackError {
  QpackError(String)
}

///|
/// Encode a QPACK string literal: the `H` bit at position `prefix_bits` (set when the
/// data is Huffman-coded), `pattern` in the bits above it, the octet length as a
/// `prefix_bits`-prefix integer, then the (raw or Huffman) octets.
pub fn qpack_encode_string(
  data : Bytes,
  prefix_bits : Int,
  pattern : Int,
  huffman : Bool,
) -> Bytes {
  let payload = if huffman { @moonrpc.huffman_encode(data) } else { data }
  let h_bit = if huffman { 1 << prefix_bits } else { 0 }
  let buf = Buffer()
  buf.write_bytes(
    qpack_int_encode(payload.length(), prefix_bits, pattern | h_bit),
  )
  buf.write_bytes(payload)
  buf.to_bytes()
}

///|
/// Decode a QPACK string literal whose length uses a `prefix_bits`-prefix integer and
/// whose `H` bit sits at position `prefix_bits`. Returns `(octets, bytes-consumed)`, or
/// `None` when the string has not fully arrived.
pub fn qpack_decode_string(
  input : BytesView,
  prefix_bits : Int,
) -> (Bytes, Int)? raise QpackError {
  if input.length() == 0 {
    return None
  }
  let huffman = ((input[0].to_int() >> prefix_bits) & 1) == 1
  let (length, len_consumed) = match qpack_int_decode(input, prefix_bits) {
    Some(v) => v
    None => return None
  }
  if input.length() < len_consumed + length {
    return None
  }
  let raw = input[len_consumed:len_consumed + length].to_owned()
  let data = if huffman {
    @moonrpc.huffman_decode(raw) catch {
      e => raise QpackError("huffman decode: " + e.to_string())
    }
  } else {
    raw
  }
  Some((data, len_consumed + length))
}

///|
/// Encode a header list as a QPACK field section over the static table only: a
/// zero-insert-count / zero-base prefix, then one field line per header — an indexed
/// static entry for an exact match, a static name reference plus a literal value when
/// the name is known, or a fully literal name and value otherwise. `huffman` chooses
/// whether literal strings are Huffman-coded.
pub fn qpack_encode_field_section(
  headers : Array[(String, String)],
  huffman? : Bool = true,
) -> Bytes {
  let buf = Buffer()
  // Encoded Field Section Prefix (RFC 9204 §4.5.1): Required Insert Count = 0 (no
  // dynamic entries), then S = 0 with Delta Base = 0.
  buf.write_byte(0)
  buf.write_byte(0)
  for header in headers {
    let (name, value) = header
    match qpack_static_find(name, value) {
      // Indexed Field Line (§4.5.2), static table: `1 T=1 | Index(6-bit prefix)`.
      Some(index) => buf.write_bytes(qpack_int_encode(index, 6, 0xc0))
      None =>
        match qpack_static_find_name(name) {
          // Literal Field Line With Name Reference (§4.5.4), static: `0 1 N=0 T=1 |
          // Name Index(4-bit prefix)`, then the value string (7-bit prefix).
          Some(index) => {
            buf.write_bytes(qpack_int_encode(index, 4, 0x50))
            buf.write_bytes(
              qpack_encode_string(@utf8.encode(value), 7, 0, huffman),
            )
          }
          // Literal Field Line With Literal Name (§4.5.6): `0 0 1 N=0 H | Name
          // Length(3-bit prefix)`, the name, then the value string.
          None => {
            buf.write_bytes(
              qpack_encode_string(@utf8.encode(name), 3, 0x20, huffman),
            )
            buf.write_bytes(
              qpack_encode_string(@utf8.encode(value), 7, 0, huffman),
            )
          }
        }
    }
  }
  buf.to_bytes()
}

///|
/// Decode a QPACK field section (encoded over the static table) back into its header
/// list. The prefix's insert count and base are read and required to be zero (this
/// decoder resolves static references only); a field line that references the dynamic
/// table is reported as unsupported.
pub fn qpack_decode_field_section(
  input : Bytes,
) -> Array[(String, String)] raise QpackError {
  let view = input[:]
  // Encoded Field Section Prefix: Required Insert Count then S + Delta Base.
  let (insert_count, ric_len) = match qpack_int_decode(view, 8) {
    Some(v) => v
    None => raise QpackError("truncated field section prefix (insert count)")
  }
  if insert_count != 0 {
    raise QpackError("non-zero Required Insert Count needs the dynamic table")
  }
  let (_base, base_len) = match qpack_int_decode(view[ric_len:], 7) {
    Some(v) => v
    None => raise QpackError("truncated field section prefix (base)")
  }
  let mut off = ric_len + base_len
  let headers : Array[(String, String)] = []
  while off < view.length() {
    let b0 = view[off].to_int()
    if (b0 & 0x80) != 0 {
      // Indexed Field Line (§4.5.2): bit 6 is T (1 = static table).
      if (b0 & 0x40) == 0 {
        raise QpackError("dynamic-table indexed field line unsupported")
      }
      let (index, consumed) = qpack_decode_int_at(view, off, 6)
      headers.push(qpack_static_entry(index))
      off += consumed
    } else if (b0 & 0x40) != 0 {
      // Literal Field Line With Name Reference (§4.5.4): bit 4 is T.
      if (b0 & 0x10) == 0 {
        raise QpackError("dynamic-table name reference unsupported")
      }
      let (index, name_consumed) = qpack_decode_int_at(view, off, 4)
      let name = qpack_static_entry(index).0
      off += name_consumed
      let (value, value_consumed) = qpack_decode_string_at(view, off, 7)
      headers.push((name, @utf8.decode_lossy(value[:])))
      off += value_consumed
    } else if (b0 & 0x20) != 0 {
      // Literal Field Line With Literal Name (§4.5.6): name string uses a 3-bit prefix.
      let (name, name_consumed) = qpack_decode_string_at(view, off, 3)
      off += name_consumed
      let (value, value_consumed) = qpack_decode_string_at(view, off, 7)
      headers.push((@utf8.decode_lossy(name[:]), @utf8.decode_lossy(value[:])))
      off += value_consumed
    } else {
      // 0b0000_xxxx: Indexed / Literal With Post-Base Index — dynamic-table only.
      raise QpackError("post-base (dynamic-table) field line unsupported")
    }
  }
  headers
}

///|
/// The static-table entry at `index`, or raise if the index is out of range.
fn qpack_static_entry(index : Int) -> (String, String) raise QpackError {
  match qpack_static_get(index) {
    Some(entry) => entry
    None => raise QpackError("static index out of range: " + index.to_string())
  }
}

///|
/// Decode a prefix integer starting at `off` in `view`, raising on truncation.
fn qpack_decode_int_at(
  view : BytesView,
  off : Int,
  prefix_bits : Int,
) -> (Int, Int) raise QpackError {
  match qpack_int_decode(view[off:], prefix_bits) {
    Some(v) => v
    None => raise QpackError("truncated field-line integer")
  }
}

///|
/// Decode a string starting at `off` in `view`, raising on truncation.
fn qpack_decode_string_at(
  view : BytesView,
  off : Int,
  prefix_bits : Int,
) -> (Bytes, Int) raise QpackError {
  match qpack_decode_string(view[off:], prefix_bits) {
    Some(v) => v
    None => raise QpackError("truncated field-line string")
  }
}