///|
/// Plan replacement metadata without copying the audio suffix.
/// Omitted pictures preserves existing pictures; [] removes all.
/// PreserveOffset fails if the remaining budget is negative or 1..3 bytes.
pub fn Document::plan(
  self : Document,
  edits : Array[TagEdit],
  pictures? : Array[Picture],
  padding? : Padding = PreserveOffset,
) -> RewritePlan raise FlacError {
  let blocks : Array[Block] = []
  let new_comments = if edits.is_empty() {
    None
  } else {
    Some(encode_comments(edit_comments(self.comments(), edits)))
  }
  let mut comment_written = false
  for block in self.blocks {
    if block.kind == 1 {
      continue
    }
    if block.kind == 6 && pictures is Some(_) {
      continue
    }
    if block.kind == 4 {
      if new_comments is Some(data) {
        blocks.push({ kind: 4, data, })
        comment_written = true
        continue
      }
    }
    blocks.push(block)
  }
  if !comment_written && new_comments is Some(data) {
    blocks.push({ kind: 4, data, })
  }
  let mut size = 4
  for block in blocks {
    guard block.data.length() <= MaxMetadata - size - 4 else {
      raise Limit("rewritten metadata exceeds budget")
    }
    size += 4 + block.data.length()
  }
  if pictures is Some(images) {
    guard images.length() <= MaxBlocks - blocks.length() else {
      raise Limit("too many pictures")
    }
    let mut icon1 = false
    let mut icon2 = false
    for picture in images {
      if picture.kind == 1 {
        guard !icon1 else { raise Invalid(0, "duplicate type 1 icon") }
        icon1 = true
      }
      if picture.kind == 2 {
        guard !icon2 else { raise Invalid(0, "duplicate type 2 icon") }
        icon2 = true
      }
      let data = encode_picture(picture)
      guard data.length() <= MaxMetadata - size - 4 else {
        raise Limit("rewritten metadata exceeds budget")
      }
      size += 4 + data.length()
      blocks.push({ kind: 6, data, })
    }
  }
  let pad = match padding {
    Compact => None
    Exact(n) => {
      guard n >= 0 && n <= MaxBlock else {
        raise Limit("invalid padding length")
      }
      Some(n)
    }
    PreserveOffset => {
      let slack = self.offset - size
      if slack == 0 {
        None
      } else {
        guard slack >= 4 else { raise NoSpace(slack) }
        Some(slack - 4)
      }
    }
  }
  if pad is Some(n) {
    guard n <= MaxMetadata - size - 4 else {
      raise Limit("padding exceeds metadata budget")
    }
    blocks.push({ kind: 1, data: Bytes::make(n, b'\x00'), })
    size += n + 4
  }
  guard blocks.length() <= MaxBlocks else {
    raise Limit("too many rewritten blocks")
  }
  let out = @buffer.Buffer()
  out.write_bytes(b"fLaC")
  for i, block in blocks {
    let n = block.data.length()
    out.write_byte(
      (block.kind | (if i == blocks.length() - 1 { 128 } else { 0 })).to_byte(),
    )
    out.write_byte((n >> 16).to_byte())
    out.write_byte((n >> 8).to_byte())
    out.write_byte(n.to_byte())
    out.write_bytes(block.data)
  }
  {
    metadata: out.to_bytes(),
    old_audio_offset: self.offset,
    audio_offset: size,
  }
}

///|
/// Build an in-memory file. Streaming hosts should use plan() instead.
pub fn Document::rewrite(
  self : Document,
  edits : Array[TagEdit],
  pictures? : Array[Picture],
  padding? : Padding = PreserveOffset,
) -> RewriteResult raise FlacError {
  let plan = self.plan(edits, pictures?, padding~)
  {
    bytes: plan.metadata + self.audio,
    old_audio_offset: plan.old_audio_offset,
    audio_offset: plan.audio_offset,
  }
}

///|
/// Plan from an exact metadata prefix, rejecting accidental audio/trailing data.
pub fn plan_metadata(
  prefix : Bytes,
  edits : Array[TagEdit],
  pictures? : Array[Picture],
  padding? : Padding = PreserveOffset,
) -> RewritePlan raise FlacError {
  let doc = parse(prefix)
  guard doc.audio.is_empty() else {
    raise Invalid(doc.offset, "plan_metadata requires an exact metadata prefix")
  }
  doc.plan(edits, pictures?, padding~)
}