// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Octets on their way out: what a caller's sink takes, a piece at a time.
///
/// The writer hands over the delimiter, the header block and the body octets of
/// a part as it is given them, so a body larger than memory never stands in it.
///
/// ```mbt check
/// test {
///   let gathered = @moonmime.Gathered::new()
///   let writer = @moonmime.MultipartWriter::new(gathered, boundary="b")
///   writer.begin_part([("content-type", "text/plain")])
///   writer.body(b"hola")
///   writer.close()
///   let parts = @moonmime.multipart(gathered.bytes()[:], "b")
///   inspect(parts[0].body, content="b\"hola\"")
/// }
/// ```
pub(open) trait Sink {
  fn write(Self, BytesView) -> Unit raise
}

///|
/// A sink that keeps what it is handed, for a caller that wants the octets
/// after all.
struct Gathered {
  held : Array[Byte]
}

///|
pub fn Gathered::new() -> Gathered {
  { held: [], }
}

///|
/// Everything written so far, as one value.
pub fn Gathered::bytes(self : Gathered) -> Bytes {
  Bytes::from_array(self.held)
}

///|
pub impl Sink for Gathered with fn write(self, chunk) {
  for i = 0; i < chunk.length(); i = i + 1 {
    self.held.push(chunk[i])
  }
}

///|
pub extend Gathered with Sink::{write}

///|
/// Writes a multipart body a part at a time: the delimiter and header block
/// when a part begins, its body octets as they are handed over, and the closing
/// delimiter when it ends.
///
/// The boundary is the caller's or a fixed stem, and the octets that would make
/// it a delimiter inside a part are watched for as they go by: a body that
/// holds the boundary is refused at that point rather than framed into a body
/// the receiver would split somewhere else. A refusal cannot take back what the
/// sink already holds, which is why a caller that cannot lose the octets passes
/// a boundary of its own.
///
/// ```mbt check
/// test {
///   let gathered = @moonmime.Gathered::new()
///   let writer = @moonmime.MultipartWriter::new(gathered, boundary="b")
///   inspect(writer.content_type(), content="multipart/mixed; boundary=b")
///   writer.begin_part([("content-type", "text/plain")])
///   writer.body(b"hola")
///   writer.begin_part([("content-type", "text/html")])
///   writer.body(b"hola")
///   writer.close()
///   let parts = @moonmime.multipart(gathered.bytes()[:], "b")
///   inspect(parts.length(), content="2")
///   inspect(parts[1].body, content="b\"hola\"")
/// }
/// ```
struct MultipartWriter {
  sink : &Sink
  kind : MultipartKind
  boundary : String
  needle : Bytes
  mut held : Array[Byte]
  mut parts : Int32
  mut open : Bool
  mut closed : Bool
}

///|
/// A writer that frames parts for `sink`.
///
/// The boundary is generated when the caller gives none: a fixed stem, so the
/// same parts always frame the same way. Unlike a body written in one call it
/// cannot be proven absent from parts that have not arrived yet, so it is
/// watched for as they go by and a collision is refused.
pub fn MultipartWriter::new(
  sink : &Sink,
  boundary? : StringView,
  kind~ : MultipartKind = Mixed,
) -> MultipartWriter raise Refused {
  let boundary = match boundary {
    Some(given) => {
      let given = given.to_owned()
      guard is_boundary(given[:]) else { raise Boundary(got=given) }
      given
    }
    None => "----moonmime"
  }
  {
    sink,
    kind,
    boundary,
    needle: ascii("--" + boundary),
    held: [],
    parts: 0,
    open: false,
    closed: false,
  }
}

///|
/// The boundary the delimiters are written with.
pub fn MultipartWriter::boundary(self : MultipartWriter) -> String {
  self.boundary
}

///|
/// The `Content-Type` that goes with the body, to be written before or beside
/// it: the subtype and the boundary the receiver splits on.
pub fn MultipartWriter::content_type(self : MultipartWriter) -> String {
  content_type_of(self.kind, self.boundary[:])
}

///|
/// Begin a part: its delimiter, then its header fields and the empty line that
/// ends them.
///
/// A part is over when the next one begins; there is nothing to write between
/// them but the delimiter, which carries the CRLF the receiver reads as the end
/// of the previous body.
pub fn MultipartWriter::begin_part(
  self : MultipartWriter,
  headers : Array[(String, String)],
) -> Unit raise {
  guard !self.closed else {
    abort("moonmime: a part began after the body closed")
  }
  let delimiter = if self.parts == 0 {
    "--" + self.boundary + "\r\n"
  } else {
    "\r\n--" + self.boundary + "\r\n"
  }
  self.sink.write(@utf8.encode(delimiter)[:])
  self.held = []
  for pair in headers {
    self.put(@utf8.encode(canonical(pair.0) + ": " + pair.1 + "\r\n"))
  }
  self.put(b"\r\n")
  self.parts = self.parts + 1
  self.open = true
}

///|
/// The next octets of the current part's body.
pub fn MultipartWriter::body(
  self : MultipartWriter,
  bytes : BytesView,
) -> Unit raise {
  guard self.open else { abort("moonmime: a body arrived before a part began") }
  self.put(bytes)
}

///|
/// Write the closing delimiter. Closing a body with no parts writes it alone,
/// which is the shape a reader takes for a body that carries none.
pub fn MultipartWriter::close(self : MultipartWriter) -> Unit raise {
  if self.closed {
    return
  }
  let delimiter = if self.parts == 0 {
    "--" + self.boundary + "--\r\n"
  } else {
    "\r\n--" + self.boundary + "--\r\n"
  }
  self.sink.write(@utf8.encode(delimiter)[:])
  self.closed = true
  self.open = false
}

///|
/// Hand octets to the sink, after watching them for the delimiter.
fn MultipartWriter::put(
  self : MultipartWriter,
  bytes : BytesView,
) -> Unit raise {
  self.watch(bytes)
  self.sink.write(bytes)
}

///|
/// Look for the delimiter in `bytes` and across the junction with what was held
/// back from the previous piece.
///
/// The junction is all a straddling delimiter can start in: a delimiter is
/// shorter than the octets a part keeps, so any match that begins earlier than
/// this piece has been seen already.
fn MultipartWriter::watch(
  self : MultipartWriter,
  bytes : BytesView,
) -> Unit raise {
  let keep = self.needle.length() - 1
  let head = if bytes.length() < keep { bytes.length() } else { keep }
  let joined : Array[Byte] = []
  for byte in self.held {
    joined.push(byte)
  }
  for i = 0; i < head; i = i + 1 {
    joined.push(bytes[i])
  }
  if index_of(Bytes::from_array(joined)[:], self.needle[:], 0) >= 0 ||
    index_of(bytes, self.needle[:], 0) >= 0 {
    raise Boundary(got=self.boundary)
  }
  let next : Array[Byte] = []
  if bytes.length() >= keep {
    for i = bytes.length() - keep; i < bytes.length(); i = i + 1 {
      next.push(bytes[i])
    }
  } else {
    let need = keep - bytes.length()
    let from = if self.held.length() > need {
      self.held.length() - need
    } else {
      0
    }
    for i = from; i < self.held.length(); i = i + 1 {
      next.push(self.held[i])
    }
    for i = 0; i < bytes.length(); i = i + 1 {
      next.push(bytes[i])
    }
  }
  self.held = next
}