///|
fn pdf_page_concat_ops(
  first : ArrayView[@content.PdfContentOp],
  second : ArrayView[@content.PdfContentOp],
) -> Array[@content.PdfContentOp] {
  [..first, ..second]
}

///|
fn pdf_page_concat3_ops(
  first : ArrayView[@content.PdfContentOp],
  second : ArrayView[@content.PdfContentOp],
  third : ArrayView[@content.PdfContentOp],
) -> Array[@content.PdfContentOp] {
  pdf_page_concat_ops(pdf_page_concat_ops(first, second), third)
}

///|
/// Return balancing graphics-state operators for an existing operator list.
///
/// If the list contains more `q` saves than `Q` restores, matching restores are
/// returned. Otherwise the result is empty.
pub fn pdf_page_protect(
  ops : ArrayView[@content.PdfContentOp],
) -> Array[@content.PdfContentOp] {
  let mut saves = 0
  let mut restores = 0
  for op in ops {
    match op {
      Op_q => saves += 1
      Op_Q => restores += 1
      _ => ()
    }
  }
  let output : Array[@content.PdfContentOp] = []
  if saves > restores {
    for _ in 0..<(saves - restores) {
      output.push(Op_Q)
    }
  }
  output
}

///|
fn PdfDocument::pdf_page_parse_content_ops(
  self : PdfDocument,
  resources : @syntax.PdfObject,
  content : ArrayView[@syntax.PdfObject],
) -> Array[@content.PdfContentOp] raise @core.PdfError {
  self.parse_content_ops_with_resources(resources, content) catch {
    DictionaryKeyNotFound(_) | ColourSpaceExpected =>
      self.parse_content_ops(content)
    error => raise error
  }
}

///|
/// Prepend content operators to a page.
///
/// In normal mode existing content is parsed and merged into a single stream
/// after `ops`. In `fast` mode a new stream is inserted before existing content
/// without parsing the old streams.
pub fn PdfDocument::prepend_operators(
  self : PdfDocument,
  page : @page.PdfPage,
  ops : ArrayView[@content.PdfContentOp],
  fast? : Bool = false,
) -> @page.PdfPage raise @core.PdfError {
  if fast {
    { ..page, content: [pdf_content_stream_of_ops(ops), ..page.content], }
  } else {
    let old_ops = self.pdf_page_parse_content_ops(page.resources, page.content)
    {
      ..page,
      content: [pdf_content_stream_of_ops(pdf_page_concat_ops(ops, old_ops))],
    }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::prepend_operators`.
pub fn pdf_prepend_operators(
  document : PdfDocument,
  operators : ArrayView[@content.PdfContentOp],
  page : @page.PdfPage,
  fast? : Bool = false,
) -> @page.PdfPage raise @core.PdfError {
  document.prepend_operators(page, operators, fast~)
}

///|
/// Append content operators to a page.
///
/// Normal mode parses existing content and wraps it with graphics-state
/// protection before appending `ops`. `fast` mode appends a protected new
/// stream without parsing existing content.
pub fn PdfDocument::postpend_operators(
  self : PdfDocument,
  page : @page.PdfPage,
  ops : ArrayView[@content.PdfContentOp],
  fast? : Bool = false,
) -> @page.PdfPage raise @core.PdfError {
  if fast {
    {
      ..page,
      content: [
        ..page.content,
        pdf_content_stream_of_ops(pdf_page_concat3_ops([Op_q], ops, [Op_Q])),
      ],
    }
  } else {
    let existing_ops = self.pdf_page_parse_content_ops(
      page.resources,
      page.content,
    )
    if existing_ops.length() == 0 {
      { ..page, content: [pdf_content_stream_of_ops(ops)], }
    } else {
      let before_ops : Array[@content.PdfContentOp] = [Op_q]
      let after_ops = pdf_page_concat3_ops(
        pdf_page_protect(existing_ops),
        [Op_Q],
        ops,
      )
      {
        ..page,
        content: [
          pdf_content_stream_of_ops(
            pdf_page_concat3_ops(before_ops, existing_ops, after_ops),
          ),
        ],
      }
    }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::postpend_operators`.
pub fn pdf_postpend_operators(
  document : PdfDocument,
  operators : ArrayView[@content.PdfContentOp],
  page : @page.PdfPage,
  fast? : Bool = false,
) -> @page.PdfPage raise @core.PdfError {
  document.postpend_operators(page, operators, fast~)
}