///|
priv enum ChunkState {
  Size
  Data(Int64)
  DataCr
  DataLf
  Trailers
  Done
}

///|
/// What one `ChunkedDecoder::feed` produced. `chunks` are nonempty payload
/// pieces whose boundaries follow the input. `trailers` holds the trailer
/// fields received so far and is complete once `done`. `consumed` counts the
/// bytes of this feed that belong to the body; bytes after it belong to the
/// next message.
#derive(Eq, Debug)
pub struct Decoded {
  /// Payload pieces, in order; never empty.
  chunks : Array[Bytes]
  /// Whether the last chunk and the trailer section ended.
  done : Bool
  /// Trailer fields, names lowercased, duplicates retained.
  trailers : @mttp_core.Headers
  /// How many bytes of this feed belong to the body.
  consumed : Int32
}

///|
/// Compares decoder results.
pub extend Decoded with Eq::{equal, not_equal}

///|
/// Formats decoder results.
pub extend Decoded with @debug.Debug::{to_repr}

///|
/// An incremental decoder of the chunked transfer coding (RFC 9112, section
/// 7.1). It keeps only the current size or trailer line and the trailer
/// fields, never a chunk's payload, so its memory is bounded by `Limits`
/// whatever chunk sizes the peer declares. Errors are sticky.
pub struct ChunkedDecoder {
  priv limits : Limits
  priv line : Buffer
  priv mut line_cr : Bool
  priv mut state : ChunkState
  priv mut trailer_bytes : Int32
  priv trailers : Array[(String, String)]
  priv mut failure : ParseError?
}

///|
/// Creates a decoder bounded by `limits.max_chunk_line_bytes` per size line
/// and by `max_trailer_bytes` and `max_headers` for the trailer section. A
/// negative limit is refused by the first `feed`.
///
/// ```mbt check
/// test {
///   let decoder = @h1.ChunkedDecoder::new(@h1.Limits::default())
///   let result = decoder.feed(b"4\r\nWiki\r\n0\r\nX-Sum: 1\r\n\r\nnext")
///   assert_eq(result.chunks, [b"Wiki"])
///   assert_true(result.done)
///   assert_eq(result.trailers.get("x-sum"), Some("1"))
///   assert_eq(result.consumed, 24)
///   decoder.finish()
/// }
/// ```
pub fn ChunkedDecoder::new(limits : Limits) -> ChunkedDecoder {
  {
    limits,
    line: Buffer(),
    line_cr: false,
    state: Size,
    trailer_bytes: 0,
    trailers: [],
    failure: None,
  }
}

///|
fn ChunkedDecoder::fail(
  self : ChunkedDecoder,
  error : ParseError,
) -> ParseError {
  self.failure = Some(error)
  self.line.reset()
  error
}

///|
/// Skips BWS (`*( SP / HTAB )`) from `i`.
fn skip_bws(line : StringView, i : Int32) -> Int32 {
  let mut j = i
  while j < line.length() && line[j] is (' ' | '\t') {
    j += 1
  }
  j
}

///|
/// Skips a nonempty token from `i`, or raises.
fn skip_token(
  line : StringView,
  i : Int32,
  what : String,
) -> Int32 raise ParseError {
  let mut j = i
  while j < line.length() && line.get_char(j) is Some(c) && is_tchar(c) {
    j += 1
  }
  if j == i {
    raise malformed("empty \{what}")
  }
  j
}

///|
/// Skips a quoted string whose opening quote is at `i - 1`.
fn skip_quoted(line : StringView, i : Int32) -> Int32 raise ParseError {
  for j = i; j < line.length(); {
    match line.get_char(j) {
      Some('"') => return j + 1
      Some('\\') => {
        guard line.get_char(j + 1) is Some(escaped) && is_field_char(escaped) else {
          raise malformed("invalid quoted-pair in chunk extension")
        }
        continue j + 2
      }
      Some(c) if is_field_char(c) => continue j + 1
      _ => raise malformed("invalid character in chunk extension")
    }
  }
  raise malformed("unterminated quoted chunk extension")
}

///|
/// Parses `chunk-size [ chunk-ext ]` (RFC 9112, section 7.1.1):
/// `chunk-ext = *( BWS ";" BWS name [ BWS "=" BWS ( token / quoted ) ] )`.
/// Extensions are validated and ignored. Whitespace is allowed only around
/// extension delimiters, never before the size or after a size alone.
fn chunk_size(line : StringView) -> Int64 raise ParseError {
  let mut size = 0L
  let mut i = 0
  while i < line.length() {
    let digit = match line[i] {
      '0'..<='9' as c => c.to_int() - '0'.to_int()
      'a'..<='f' as c => c.to_int() - 'a'.to_int() + 10
      'A'..<='F' as c => c.to_int() - 'A'.to_int() + 10
      _ => break
    }
    if size > (9223372036854775807L - digit.to_int64()) / 16L {
      raise malformed("chunk size overflow")
    }
    size = size * 16L + digit.to_int64()
    i += 1
  }
  if i == 0 {
    raise malformed("invalid chunk size")
  }
  while i < line.length() {
    i = skip_bws(line, i)
    guard i < line.length() && line[i] == ';' else {
      raise malformed("invalid chunk size or extension")
    }
    i = skip_token(line, skip_bws(line, i + 1), "chunk extension name")
    let j = skip_bws(line, i)
    if j < line.length() && line[j] == '=' {
      let k = skip_bws(line, j + 1)
      i = if k < line.length() && line[k] == '"' {
        skip_quoted(line, k + 1)
      } else {
        skip_token(line, k, "chunk extension value")
      }
    }
  }
  size
}

///|
/// Trailer fields that would change framing or routing if a recipient
/// merged them into the head (RFC 9110, section 6.5.1).
let forbidden_trailers : Array[String] = [
  "content-length", "transfer-encoding", "host", "connection", "trailer",
]

///|
/// Consumes the bytes of `bytes` that belong to the body and returns the
/// payload they carried. Report the end of input with `finish`: a zero-size
/// chunk is not complete until the trailer section's final CRLF arrives.
/// A size line or trailer section past its limit raises `HeadTooLarge`; a
/// trailer that names a framing or routing field raises `Smuggling`.
pub fn ChunkedDecoder::feed(
  self : ChunkedDecoder,
  bytes : BytesView,
) -> Decoded raise ParseError {
  if self.failure is Some(error) {
    raise error
  }
  if self.limits.max_chunk_line_bytes < 0 ||
    self.limits.max_trailer_bytes < 0 ||
    self.limits.max_headers < 0 {
    raise self.fail(malformed("negative chunk limits"))
  }
  let chunks = []
  let mut i = 0
  while i < bytes.length() {
    match self.state {
      Done => break
      Data(remaining) => {
        let count = remaining.min((bytes.length() - i).to_int64()).to_int()
        chunks.push(bytes[i:i + count].to_owned())
        i += count
        self.state = if remaining == count.to_int64() {
          DataCr
        } else {
          Data(remaining - count.to_int64())
        }
      }
      DataCr => {
        if bytes[i] != b'\r' {
          raise self.fail(malformed("chunk data lacks CRLF"))
        }
        i += 1
        self.state = DataLf
      }
      DataLf => {
        if bytes[i] != b'\n' {
          raise self.fail(malformed("chunk data lacks CRLF"))
        }
        i += 1
        self.state = Size
      }
      Size | Trailers => {
        let byte = bytes[i]
        i += 1
        self.line_byte(byte) catch {
          ParseError(..) as error => raise self.fail(error)
        }
      }
    }
  }
  {
    chunks,
    done: self.state is Done,
    trailers: @mttp_core.Headers::of(self.trailers),
    consumed: i,
  }
}

///|
/// Adds one byte to the current size or trailer line and handles the line
/// once its CRLF arrived.
fn ChunkedDecoder::line_byte(
  self : ChunkedDecoder,
  byte : Byte,
) -> Unit raise ParseError {
  let is_trailer = self.state is Trailers
  if is_trailer {
    if self.trailer_bytes >= self.limits.max_trailer_bytes {
      raise too_large(
        "trailer section exceeds \{self.limits.max_trailer_bytes} bytes",
      )
    }
    self.trailer_bytes += 1
  } else if self.line.length() >= self.limits.max_chunk_line_bytes {
    raise too_large(
      "chunk size line exceeds \{self.limits.max_chunk_line_bytes} bytes",
    )
  }
  if (self.line_cr && byte != b'\n') || (byte == b'\n' && !self.line_cr) {
    raise malformed("chunk line requires CRLF")
  }
  self.line.write_byte(byte)
  self.line_cr = byte == b'\r'
  if byte != b'\n' {
    return
  }
  let data = self.line.to_bytes()
  self.line.reset()
  let line = latin1(data[:data.length() - 2])
  if !is_trailer {
    let size = chunk_size(line)
    self.state = if size == 0L { Trailers } else { Data(size) }
  } else if line.is_empty() {
    self.state = Done
  } else {
    if self.trailers.length() >= self.limits.max_headers {
      raise too_large("more than \{self.limits.max_headers} trailer fields")
    }
    let pair = field(line)
    if forbidden_trailers.contains(pair.0) {
      raise ParseError(kind=Smuggling, detail="framing field in trailers")
    }
    self.trailers.push(pair)
  }
}

///|
/// Checks at end of input that the body ended; a truncated body raises, and
/// the error is sticky.
pub fn ChunkedDecoder::finish(self : ChunkedDecoder) -> Unit raise ParseError {
  if self.failure is Some(error) {
    raise error
  }
  if !(self.state is Done) {
    raise self.fail(malformed("truncated chunked body"))
  }
}