///|
/// Input format selector for the streaming reader.
pub(all) enum CpioFormat {
  Newc
  Odc
  BinaryOld
  BinaryNew(BinaryByteOrder)
} derive(Eq, Debug)

///|
/// Format-specific metadata delivered for each streamed entry.
pub(all) enum StreamHeader {
  Newc(Header)
  Odc(OdcHeader)
  Binary(BinaryHeader)
} derive(Eq, Debug)

///|
/// One zero-copy entry delivered by `stream_entries`.
///
/// `name_bytes` and `data` are views into the input buffer and remain valid
/// only while that buffer is alive.
pub(all) struct StreamEntry {
  name : String
  name_bytes : BytesView
  header : StreamHeader
  data : BytesView
} derive(Eq, Debug)

///|
fn stream_newc(
  input : Bytes,
  on_entry : (StreamEntry) -> Unit,
) -> Result[Unit, CpioError] {
  let mut format = NewcFormat::Newc
  let mut has_format = false
  let mut offset = 0
  while true {
    let header_end = offset + NEWC_HEADER_SIZE
    if header_end > input.length() {
      return Err(TruncatedHeader(offset))
    }
    let header = match parse_header(input[offset:header_end], offset) {
      Ok(header) => header
      Err(error) => return Err(error)
    }
    if has_format {
      if header.format != format {
        return Err(MixedFormats(offset))
      }
    } else {
      format = header.format
      has_format = true
    }

    let name_start = header_end
    let remaining = (input.length() - name_start).to_uint64()
    if header.name_size == 0UL || header.name_size > remaining {
      return Err(InvalidNameSize(offset))
    }
    let name_size = header.name_size.to_int()
    let name_end = name_start + name_size
    if input[name_end - 1] != b'\x00' {
      return Err(MissingNameTerminator(name_start))
    }
    let name_view = input[name_start:name_end - 1]
    for i = 0; i < name_view.length(); i = i + 1 {
      if name_view[i] == b'\x00' {
        return Err(EmbeddedNul(name_start + i))
      }
    }
    let name = @utf8.decode(name_view) catch {
      _ => return Err(InvalidUtf8Name(name_start))
    }

    let data_start = name_end + newc_name_padding(name_size)
    if data_start > input.length() {
      return Err(TruncatedName(name_end))
    }
    if name == "TRAILER!!!" {
      if header.file_size != 0UL {
        return Err(InvalidTrailer(offset))
      }
      if header.format == NewcCrc && header.check != 0UL {
        return Err(ChecksumMismatch(offset, header.check, 0UL))
      }
      return Ok(())
    }

    let data_remaining = (input.length() - data_start).to_uint64()
    if header.file_size > data_remaining {
      return Err(TruncatedData(data_start))
    }
    let file_size = header.file_size.to_int()
    let data_end = data_start + file_size
    let next_offset = data_end + padding_for(file_size)
    if next_offset > input.length() {
      return Err(TruncatedData(data_end))
    }
    let data = input[data_start:data_end]
    if header.format == NewcCrc {
      let actual = newc_checksum(data)
      if header.check != actual {
        return Err(ChecksumMismatch(data_start, header.check, actual))
      }
    }
    on_entry({
      name,
      name_bytes: name_view,
      header: StreamHeader::Newc(header),
      data,
    })
    offset = next_offset
  } nobreak {
    abort("unreachable: stream_newc always returns")
  }
}

///|
fn stream_odc(
  input : Bytes,
  on_entry : (StreamEntry) -> Unit,
) -> Result[Unit, CpioError] {
  let mut offset = 0
  while true {
    let header_end = offset + ODC_HEADER_SIZE
    if header_end > input.length() {
      return Err(TruncatedHeader(offset))
    }
    let header = match parse_odc_header(input[offset:header_end], offset) {
      Ok(header) => header
      Err(error) => return Err(error)
    }

    let name_start = header_end
    let remaining = (input.length() - name_start).to_uint64()
    if header.name_size == 0UL || header.name_size > remaining {
      return Err(InvalidNameSize(offset))
    }
    let name_size = header.name_size.to_int()
    let name_end = name_start + name_size
    if input[name_end - 1] != b'\x00' {
      return Err(MissingNameTerminator(name_start))
    }
    let name_view = input[name_start:name_end - 1]
    for i = 0; i < name_view.length(); i = i + 1 {
      if name_view[i] == b'\x00' {
        return Err(EmbeddedNul(name_start + i))
      }
    }
    let name = @utf8.decode(name_view) catch {
      _ => return Err(InvalidUtf8Name(name_start))
    }

    if name == "TRAILER!!!" {
      if header.file_size != 0UL {
        return Err(InvalidTrailer(offset))
      }
      return Ok(())
    }

    let data_remaining = (input.length() - name_end).to_uint64()
    if header.file_size > data_remaining {
      return Err(TruncatedData(name_end))
    }
    let data_end = name_end + header.file_size.to_int()
    on_entry({
      name,
      name_bytes: name_view,
      header: StreamHeader::Odc(header),
      data: input[name_end:data_end],
    })
    offset = data_end
  } nobreak {
    abort("unreachable: stream_odc always returns")
  }
}

///|
fn stream_binary(
  input : Bytes,
  variant : BinaryVariant,
  byte_order : BinaryByteOrder,
  on_entry : (StreamEntry) -> Unit,
) -> Result[Unit, CpioError] {
  let mut offset = 0
  while true {
    let header_end = offset + BINARY_HEADER_SIZE
    if header_end > input.length() {
      return Err(TruncatedHeader(offset))
    }
    let header = match
      parse_binary_header(input[offset:header_end], variant, byte_order, offset) {
      Ok(header) => header
      Err(error) => return Err(error)
    }

    let name_start = header_end
    let remaining = (input.length() - name_start).to_uint64()
    if header.name_size == 0UL || header.name_size > remaining {
      return Err(InvalidNameSize(offset))
    }
    let name_size = header.name_size.to_int()
    let name_end = name_start + name_size
    if input[name_end - 1] != b'\x00' {
      return Err(MissingNameTerminator(name_start))
    }
    let name_view = input[name_start:name_end - 1]
    for i = 0; i < name_view.length(); i = i + 1 {
      if name_view[i] == b'\x00' {
        return Err(EmbeddedNul(name_start + i))
      }
    }
    let name = @utf8.decode(name_view) catch {
      _ => return Err(InvalidUtf8Name(name_start))
    }

    let data_start = name_end + binary_padding(name_size)
    if data_start > input.length() {
      return Err(TruncatedName(name_end))
    }
    if name == "TRAILER!!!" {
      if header.file_size != 0UL {
        return Err(InvalidTrailer(offset))
      }
      return Ok(())
    }

    let data_remaining = (input.length() - data_start).to_uint64()
    if header.file_size > data_remaining {
      return Err(TruncatedData(data_start))
    }
    let file_size = header.file_size.to_int()
    let data_end = data_start + file_size
    let next_offset = data_end + binary_padding(file_size)
    if next_offset > input.length() {
      return Err(TruncatedData(data_end))
    }
    on_entry({
      name,
      name_bytes: name_view,
      header: StreamHeader::Binary(header),
      data: input[data_start:data_end],
    })
    offset = next_offset
  } nobreak {
    abort("unreachable: stream_binary always returns")
  }
}

///|
/// Streams entries one at a time without constructing an archive array or
/// copying entry data.
///
/// The callback executes synchronously for each entry. Binary old/new layout
/// ambiguity is resolved by the explicit `CpioFormat`.
pub fn stream_entries(
  input : Bytes,
  format : CpioFormat,
  on_entry : (StreamEntry) -> Unit,
) -> Result[Unit, CpioError] {
  match format {
    Newc => stream_newc(input, on_entry)
    Odc => stream_odc(input, on_entry)
    BinaryOld => stream_binary(input, Old, Little, on_entry)
    BinaryNew(byte_order) => stream_binary(input, New, byte_order, on_entry)
  }
}