///|
/// One completed mboxrd record. Source offsets include envelope/quoting/separator;
/// content only removes reversible From quoting and ONE container blank line.
/// Bytes are owned snapshots, never aliases into the decoder's mutable buffers.
pub struct RawMessage {
  number : Int64
  source_start : Int64
  source_end : Int64
  envelope : Bytes
  content : Bytes
} derive(Debug, Eq)

///|
/// Incremental container decoder, independent of MIME. One 1MiB input chunk,
/// one bounded line and one bounded message at a time; callers drain results.
pub struct MboxrdDecoder {
  priv max_message_bytes : Int
  priv max_line_bytes : Int
  priv line : @buffer.Buffer
  priv body : @buffer.Buffer
  priv mut blank : Bytes
  priv mut envelope : Bytes?
  priv mut offset : Int64
  priv mut line_start : Int64
  priv mut message_start : Int64
  priv mut number : Int64
  priv mut closed : Bool
  priv mut failed : Bool
}

///|
/// Budgets count DECODED content bytes and physical line bytes including LF.
/// No whole-file size limit; offsets and record numbers use signed 64-bit ints.
pub fn MboxrdDecoder::new(
  max_message_bytes? : Int = 33554432,
  max_line_bytes? : Int = 1048576,
) -> MboxrdDecoder raise MailboxError {
  if max_message_bytes < 0 ||
    max_message_bytes > 33554432 ||
    max_line_bytes < 6 ||
    max_line_bytes > 1048576 {
    raise Invalid("mboxrd budgets: message 0..32MiB, physical line 6..1MiB")
  }
  {
    max_message_bytes,
    max_line_bytes,
    line: @buffer.Buffer(),
    body: @buffer.Buffer(),
    blank: b"",
    envelope: None,
    offset: 0L,
    line_start: 0L,
    message_start: 0L,
    number: 0L,
    closed: false,
    failed: false,
  }
}

///|
fn MboxrdDecoder::append_body(
  self : MboxrdDecoder,
  data : BytesView,
) -> Unit raise MailboxError {
  if data.length() > self.max_message_bytes - self.body.length() {
    raise Invalid("mboxrd message byte budget exceeded")
  }
  self.body.write_bytes(data)
}

///|
fn MboxrdDecoder::complete(
  self : MboxrdDecoder,
  end : Int64,
) -> RawMessage raise MailboxError {
  if self.blank.is_empty() {
    raise Invalid("mboxrd message missing final container blank line")
  }
  let envelope = match self.envelope {
    Some(bytes) => bytes
    None => raise Invalid("mboxrd message missing envelope")
  }
  // A pending final blank is the CONTAINER separator, not part of the content.
  let message = RawMessage::{
    number: self.number,
    source_start: self.message_start,
    source_end: end,
    envelope,
    content: self.body.to_bytes(),
  }
  self.body.reset()
  self.blank = b""
  message
}

///|
fn MboxrdDecoder::accept_line(
  self : MboxrdDecoder,
  out : Array[RawMessage],
) -> Unit raise MailboxError {
  let bytes = self.line.to_bytes()
  self.line.reset()
  let end = if bytes.length() >= 2 && bytes[bytes.length() - 2] == b'\r' {
    bytes.length() - 2
  } else {
    bytes.length() - 1
  }
  if bytes.has_prefix(b"From ") {
    if self.envelope is Some(_) {
      out.push(self.complete(self.line_start))
    }
    if end <= 5 {
      raise Invalid("mboxrd empty envelope")
    }
    let mut nonblank = false
    for i in 5..' {
        quoted += 1
      }
      let skip = if quoted > 0 && bytes[quoted:].has_prefix(b"From ") {
        1
      } else {
        0
      }
      self.append_body(bytes[skip:])
    }
  }
  self.line_start = self.offset
}

///|
fn MboxrdDecoder::poison(self : MboxrdDecoder) -> Unit {
  self.failed = true
  self.line.reset()
  self.body.reset()
  self.envelope = None
  self.blank = b""
}

///|
/// Results are complete records only. Any error poisons the entire decoder;
/// previously returned records stay valid, but this call returns no partial array.
pub fn MboxrdDecoder::feed(
  self : MboxrdDecoder,
  chunk : Bytes,
) -> Array[RawMessage] raise MailboxError {
  if self.failed || self.closed {
    raise Invalid("mboxrd decoder is failed or finished")
  }
  errdefer self.poison()
  if chunk.length() > 1048576 {
    raise Invalid("mboxrd feed chunk exceeds 1MiB")
  }
  if self.offset > 9223372036854775807L - chunk.length().to_int64() {
    raise Invalid("mboxrd byte offset exhausted")
  }
  let out : Array[RawMessage] = []
  for byte in chunk {
    if self.line.length() == self.max_line_bytes {
      raise Invalid("mboxrd physical line byte budget exceeded")
    }
    self.line.write_byte(byte)
    self.offset += 1L
    if byte == b'\n' {
      self.accept_line(out)
    }
  }
  out
}

///|
/// Strict EOF: a nonempty partial line or a missing container blank is truncated.
/// The empty mailbox returns [], and successful completion closes the decoder.
pub fn MboxrdDecoder::finish(
  self : MboxrdDecoder,
) -> Array[RawMessage] raise MailboxError {
  if self.failed || self.closed {
    raise Invalid("mboxrd decoder is failed or finished")
  }
  errdefer self.poison()
  if !self.line.is_empty() {
    raise Invalid("mboxrd truncated final physical line")
  }
  let result = match self.envelope {
    None => []
    Some(_) => [self.complete(self.offset)]
  }
  self.closed = true
  self.envelope = None
  result
}