///|
/// State-machine wrapper around `MultipartWriter` that binds one emitted
/// message to a previously validated content-length plan.
pub struct PlannedEmitter {
  priv plan : MultipartPlan
  priv writer : MultipartWriter
  priv mut next_part : Int
  priv mut active_part : Int?
  priv mut active_bytes : Int64
  priv mut done : Bool
  priv mut failed : Bool
}

///|
fn[T] PlannedEmitter::fail(
  self : PlannedEmitter,
  error : @model.MultipartError,
) -> T raise @model.MultipartError {
  self.failed = true
  raise error
}

///|
fn PlannedEmitter::invalid_state(
  self : PlannedEmitter,
  operation : String,
) -> @model.MultipartError {
  let phase = if self.failed {
    @model.ParserPhase::Failed
  } else if self.done {
    Done
  } else {
    Body
  }
  InvalidState(operation~, phase~)
}

///|
pub fn PlannedEmitter::new(
  plan : MultipartPlan,
) -> PlannedEmitter raise @model.MultipartError {
  let writer = MultipartWriter::new(plan.boundary, plan.limits)
  {
    plan,
    writer,
    next_part: 0,
    active_part: None,
    active_bytes: 0L,
    done: false,
    failed: false,
  }
}

///|
pub fn PlannedEmitter::begin_next(
  self : PlannedEmitter,
) -> Bytes raise @model.MultipartError {
  if self.failed ||
    self.done ||
    self.active_part is Some(_) ||
    self.next_part >= self.plan.parts.length() {
    self.fail(self.invalid_state("begin_next"))
  }
  let part = self.plan.parts[self.next_part]
  let opening = self.writer.begin_part(part.meta) catch {
    error => self.fail(error)
  }
  self.active_part = Some(self.next_part)
  self.active_bytes = 0L
  opening
}

///|
pub fn PlannedEmitter::write_chunk(
  self : PlannedEmitter,
  chunk : Bytes,
) -> Bytes raise @model.MultipartError {
  if self.failed || self.done {
    self.fail(self.invalid_state("write_chunk"))
  }
  let index = match self.active_part {
    Some(index) => index
    None => self.fail(self.invalid_state("write_chunk"))
  }
  let planned = self.plan.parts[index]
  let observed = self.active_bytes + chunk.length().to_int64()
  if observed > planned.body_length {
    self.fail(
      BodyLengthMismatch(
        part_index=planned.meta.index,
        expected=planned.body_length,
        observed~,
      ),
    )
  }
  let output = self.writer.write(chunk) catch { error => self.fail(error) }
  self.active_bytes = observed
  output
}

///|
pub fn PlannedEmitter::end_next(
  self : PlannedEmitter,
) -> Bytes raise @model.MultipartError {
  if self.failed || self.done {
    self.fail(self.invalid_state("end_next"))
  }
  let index = match self.active_part {
    Some(index) => index
    None => self.fail(self.invalid_state("end_next"))
  }
  let planned = self.plan.parts[index]
  if self.active_bytes != planned.body_length {
    self.fail(
      BodyLengthMismatch(
        part_index=planned.meta.index,
        expected=planned.body_length,
        observed=self.active_bytes,
      ),
    )
  }
  let ending = self.writer.end_part() catch { error => self.fail(error) }
  self.active_part = None
  self.active_bytes = 0L
  self.next_part += 1
  ending
}

///|
pub fn PlannedEmitter::finish(
  self : PlannedEmitter,
) -> Bytes raise @model.MultipartError {
  if self.failed ||
    self.done ||
    self.active_part is Some(_) ||
    self.next_part != self.plan.parts.length() {
    self.fail(self.invalid_state("finish"))
  }
  let closing = self.writer.finish() catch { error => self.fail(error) }
  self.done = true
  closing
}