///|
const WRITER_READY : Int = 0

///|
const WRITER_IN_PART : Int = 1

///|
const WRITER_BETWEEN_PARTS : Int = 2

///|
const WRITER_DONE : Int = 3

///|
const WRITER_FAILED : Int = 4

///|
pub struct MultipartWriter {
  priv boundary : String
  priv limits : @model.Limits
  priv mut state : Int
  priv mut part_count : Int
  priv mut current_part_bytes : Int64
  priv mut emitted : Int64
}

///|
fn[T] MultipartWriter::raise_failed(
  self : MultipartWriter,
  error : @model.MultipartError,
) -> T raise @model.MultipartError {
  self.state = WRITER_FAILED
  raise error
}

///|
fn MultipartWriter::invalid_state(
  self : MultipartWriter,
  operation : String,
) -> @model.MultipartError {
  let phase = if self.state == WRITER_FAILED {
    @model.ParserPhase::Failed
  } else if self.state == WRITER_DONE {
    Done
  } else if self.state == WRITER_IN_PART {
    Body
  } else {
    Headers
  }
  InvalidState(operation~, phase~)
}

///|
fn MultipartWriter::record_output(
  self : MultipartWriter,
  bytes : Bytes,
) -> Bytes raise @model.MultipartError {
  let observed = self.emitted + bytes.length().to_int64()
  if observed > self.limits.max_total_bytes {
    self.raise_failed(
      LimitExceeded(
        kind=TotalBytes,
        limit=self.limits.max_total_bytes,
        observed~,
      ),
    )
  }
  self.emitted = observed
  bytes
}

///|
fn concat_bytes(parts : Array[Bytes]) -> Bytes {
  let output = Buffer()
  for part in parts {
    output.write_bytes(part)
  }
  output.to_bytes()
}

///|
pub fn MultipartWriter::new(
  boundary : String,
  limits : @model.Limits,
) -> MultipartWriter raise @model.MultipartError {
  @syntax.validate_boundary(boundary)
  @model.validate_limits(limits)
  {
    boundary,
    limits,
    state: WRITER_READY,
    part_count: 0,
    current_part_bytes: 0L,
    emitted: 0L,
  }
}

///|
pub fn MultipartWriter::content_type(self : MultipartWriter) -> String {
  @syntax.format_content_type(self.boundary) catch {
    _ => "multipart/form-data"
  }
}

///|
pub fn MultipartWriter::begin_part(
  self : MultipartWriter,
  meta : @model.PartMeta,
) -> Bytes raise @model.MultipartError {
  if self.state != WRITER_READY && self.state != WRITER_BETWEEN_PARTS {
    self.raise_failed(self.invalid_state("begin_part"))
  }
  let observed_parts = self.part_count + 1
  if observed_parts > self.limits.max_parts {
    self.raise_failed(
      LimitExceeded(
        kind=Parts,
        limit=self.limits.max_parts.to_int64(),
        observed=observed_parts.to_int64(),
      ),
    )
  }
  let normalized_meta = { ..meta, index: self.part_count }
  let headers = @syntax.format_part_headers(normalized_meta) catch {
    error => self.raise_failed(error)
  }
  let delimiter = if self.state == WRITER_READY {
    @utf8.encode("--\{self.boundary}\r\n")
  } else {
    @utf8.encode("\r\n--\{self.boundary}\r\n")
  }
  let output = concat_bytes([delimiter, headers, b"\r\n"])
  let recorded = self.record_output(output)
  self.part_count = observed_parts
  self.current_part_bytes = 0L
  self.state = WRITER_IN_PART
  recorded
}

///|
pub fn MultipartWriter::write(
  self : MultipartWriter,
  chunk : Bytes,
) -> Bytes raise @model.MultipartError {
  if self.state != WRITER_IN_PART {
    self.raise_failed(self.invalid_state("write"))
  }
  let observed = self.current_part_bytes + chunk.length().to_int64()
  if observed > self.limits.max_part_bytes {
    self.raise_failed(
      LimitExceeded(kind=PartBytes, limit=self.limits.max_part_bytes, observed~),
    )
  }
  let recorded = self.record_output(chunk)
  self.current_part_bytes = observed
  recorded
}

///|
pub fn MultipartWriter::end_part(
  self : MultipartWriter,
) -> Bytes raise @model.MultipartError {
  if self.state != WRITER_IN_PART {
    self.raise_failed(self.invalid_state("end_part"))
  }
  self.state = WRITER_BETWEEN_PARTS
  b""
}

///|
pub fn MultipartWriter::finish(
  self : MultipartWriter,
) -> Bytes raise @model.MultipartError {
  if self.state != WRITER_READY && self.state != WRITER_BETWEEN_PARTS {
    self.raise_failed(self.invalid_state("finish"))
  }
  let closing = if self.state == WRITER_READY {
    @utf8.encode("--\{self.boundary}--\r\n")
  } else {
    @utf8.encode("\r\n--\{self.boundary}--\r\n")
  }
  let recorded = self.record_output(closing)
  self.state = WRITER_DONE
  recorded
}

///|
pub fn MultipartWriter::bytes_emitted(self : MultipartWriter) -> Int64 {
  self.emitted
}