///|
/// Events emitted by the binary multipart decoder. Part bodies remain Bytes,
/// so file contents are never decoded as text.
pub(all) enum StreamEvent {
  PartBegin(Array[Header])
  PartData(Bytes)
  PartEnd
  Finished
} derive(Debug)

///|
/// Decodes a complete multipart body into binary-preserving events. This is
/// the one-shot companion to the upcoming incremental byte state machine.
pub fn decode_bytes(
  boundary : String,
  body : Bytes,
) -> Result[Array[StreamEvent], MultipartError] {
  guard is_valid_boundary(boundary) else {
    return Err(MultipartError::InvalidBoundary)
  }
  let opening = @utf8.encode("--" + boundary + "\r\n")
  guard body.has_prefix(opening[:]) else {
    return Err(MultipartError::MissingOpeningBoundary)
  }
  let marker = @utf8.encode("\r\n--" + boundary)
  let events : Array[StreamEvent] = []
  for cursor = opening.length() {
    let remaining = body.view(start=cursor)
    guard remaining.find(b"\r\n\r\n") is Some(header_offset) else {
      return Err(MultipartError::MissingHeaderTerminator)
    }
    let headers = match
      parse_headers(@utf8.decode_lossy(remaining.view(end=header_offset))) {
      Ok(parsed) => parsed
      Err(error) => return Err(error)
    }
    match validate_form_data_headers(headers) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
    events.push(StreamEvent::PartBegin(headers))
    let content_start = cursor + header_offset + 4
    guard find_byte_delimiter(body, marker, content_start)
      is Some((content_end, after_marker)) else {
      return Err(MultipartError::MissingClosingBoundary)
    }
    let data = body.view(start=content_start, end=content_end).to_owned()
    if !data.is_empty() {
      events.push(StreamEvent::PartData(data))
    }
    events.push(StreamEvent::PartEnd)
    let suffix = body.view(start=after_marker)
    if suffix.has_prefix(b"--") {
      let tail = suffix.view(start=2)
      if tail.length() == 0 || tail.equal_to_bytes(b"\r\n") {
        events.push(StreamEvent::Finished)
        return Ok(events)
      }
      return Err(MultipartError::UnexpectedBoundary)
    }
    if suffix.has_prefix(b"\r\n") {
      continue after_marker + 2
    }
    return Err(MultipartError::UnexpectedBoundary)
  }
}

///|
fn find_byte_delimiter(
  body : Bytes,
  marker : Bytes,
  start : Int,
) -> (Int, Int)? {
  for scan = start {
    let remaining = body.view(start=scan)
    guard remaining.find(marker[:]) is Some(offset) else { return None }
    let delimiter_start = scan + offset
    let after_marker = delimiter_start + marker.length()
    let suffix = body.view(start=after_marker)
    if suffix.has_prefix(b"--") || suffix.has_prefix(b"\r\n") {
      return Some((delimiter_start, after_marker))
    }
    continue after_marker
  }
}