///|
fn pdf_xobject_name(text : String) -> @core.PdfName {
  @core.pdf_name_of_bytes(@ascii.encode(text))
}

///|
let pdf_xobject_type_key_name : @core.PdfName = pdf_xobject_name("/Type")

///|
fn pdf_xobject_type_key() -> @core.PdfName {
  pdf_xobject_type_key_name
}

///|
let pdf_xobject_xobject_name : @core.PdfName = pdf_xobject_name("/XObject")

///|
fn pdf_xobject_xobject() -> @core.PdfName {
  pdf_xobject_xobject_name
}

///|
let pdf_xobject_subtype_key_name : @core.PdfName = pdf_xobject_name("/Subtype")

///|
fn pdf_xobject_subtype_key() -> @core.PdfName {
  pdf_xobject_subtype_key_name
}

///|
let pdf_xobject_form_name : @core.PdfName = pdf_xobject_name("/Form")

///|
fn pdf_xobject_form() -> @core.PdfName {
  pdf_xobject_form_name
}

///|
let pdf_xobject_bbox_key_name : @core.PdfName = pdf_xobject_name("/BBox")

///|
fn pdf_xobject_bbox_key() -> @core.PdfName {
  pdf_xobject_bbox_key_name
}

///|
let pdf_xobject_matrix_key_name : @core.PdfName = pdf_xobject_name("/Matrix")

///|
fn pdf_xobject_matrix_key() -> @core.PdfName {
  pdf_xobject_matrix_key_name
}

///|
let pdf_xobject_resources_key_name : @core.PdfName = pdf_xobject_name(
  "/Resources",
)

///|
fn pdf_xobject_resources_key() -> @core.PdfName {
  pdf_xobject_resources_key_name
}

///|
let pdf_xobject_length_key_name : @core.PdfName = pdf_xobject_name("/Length")

///|
fn pdf_xobject_length_key() -> @core.PdfName {
  pdf_xobject_length_key_name
}

///|
let pdf_xobject_id_key_name : @core.PdfName = pdf_xobject_name("/ID")

///|
fn pdf_xobject_id_key() -> @core.PdfName {
  pdf_xobject_id_key_name
}

///|
fn pdf_xobject_all_page_range(
  document : PdfDocument,
) -> Array[Int] raise @core.PdfError {
  let count = document.pages_of_pagetree().length()
  [
    for page_number in 1..<=count => page_number
  ]
}

///|
fn pdf_xobject_identity_changes(count : Int) -> Array[(Int, Int)] {
  [
    for page_number in 1..<=count => (page_number, page_number)
  ]
}

///|
fn pdf_xobject_all_but_last_page(
  pages : ArrayView[@page.PdfPage],
) -> Array[@page.PdfPage] {
  [
    for index in 0..<(pages.length() - 1) => pages[index]
  ]
}

///|
fn PdfDocument::pdf_xobject_form_object_of_page(
  self : PdfDocument,
  page : @page.PdfPage,
) -> @syntax.PdfObject raise @core.PdfError {
  let ops = self.parse_content_ops_with_resources(page.resources, page.content)
  let data = pdf_content_bytes_of_ops(ops)
  @syntax.pdf_stream(
    PdfDictionary([
      (pdf_xobject_type_key(), PdfNameObject(pdf_xobject_xobject())),
      (pdf_xobject_subtype_key(), PdfNameObject(pdf_xobject_form())),
      (pdf_xobject_bbox_key(), page.mediabox),
      (pdf_xobject_resources_key(), page.resources),
      (pdf_xobject_length_key(), PdfInteger(data.length())),
    ]),
    StreamGot(data),
  )
}

///|
fn PdfDocument::pdf_xobject_add_to_page(
  self : PdfDocument,
  page : @page.PdfPage,
  name : @core.PdfName,
  object_number : Int,
) -> @page.PdfPage raise @core.PdfError {
  let xobjects = match
    self.lookup_direct(pdf_xobject_xobject(), page.resources) {
    Some(value) => value
    None => PdfDictionary([])
  }
  let xobjects = xobjects.add_dict_entry(name, PdfIndirect(object_number))
  {
    ..page,
    resources: page.resources.add_dict_entry(pdf_xobject_xobject(), xobjects),
  }
}

///|
fn PdfDocument::pdf_xobject_add_page_as_xobject(
  self : PdfDocument,
  range : ArrayView[Int],
  page : @page.PdfPage,
  name : @core.PdfName,
) -> PdfDocument raise @core.PdfError {
  let pdf = self.copy()
  let xobject_number = pdf.add_object(pdf.pdf_xobject_form_object_of_page(page))
  let pages = pdf.pages_of_pagetree()
  let selected_pages = @core.pdf_number_set(range)
  let rewritten : Array[@page.PdfPage] = Array(capacity=pages.length())
  for index in 0.. Unit raise @core.PdfError {
  match source.lookup_direct(pdf_xobject_id_key(), source.trailer_dict()) {
    Some(id) =>
      self.set_trailer_dict(
        self.trailer_dict().add_dict_entry(pdf_xobject_id_key(), id),
      )
    None => ()
  }
}

///|
fn pdf_xobject_retained_bookmarks(
  bookmarks : ArrayView[@bookmark.PdfBookmark],
  old_page_numbers : ArrayView[Int],
  new_page_numbers : ArrayView[Int],
) -> Array[@bookmark.PdfBookmark] raise @core.PdfError {
  let change_map = pdf_page_reference_map(
    pdf_page_reference_changes(old_page_numbers, new_page_numbers),
  )
  [
    for bookmark in bookmarks => {
      pdf_merge_renumber_bookmark_with_map(bookmark, change_map)
    }
  ]
}

///|
/// Stamp the first page of `overlay` into this document as a Form XObject.
///
/// The returned name is the resource name added to each selected base page.
/// The overlay page itself is not retained as a visible page; its content and
/// resources are embedded in the new Form XObject. Existing bookmarks in the
/// base document are preserved and retargeted to the rebuilt page tree.
pub fn PdfDocument::stamp_as_xobject(
  self : PdfDocument,
  range : ArrayView[Int],
  overlay : PdfDocument,
) -> (PdfDocument, @core.PdfName) raise @core.PdfError {
  let prefix = self.shortest_unused_prefix()
  let overlay = overlay.copy()
  overlay.add_prefix(prefix)
  let bookmarks = self.read_bookmarks(preserve_actions=true) catch { _ => [] }
  let bookmark_page_numbers = self.page_reference_numbers()
  let base = self.remove_bookmarks()
  let overlay = overlay.remove_bookmarks()
  let base_range = pdf_xobject_all_page_range(base)
  let overlay_first_page = overlay.pdf_of_pages([1])
  let merged = pdf_merge_pdfs(
    false,
    false,
    ["a", "b"],
    [base, overlay_first_page],
    [base_range, [1]],
  )
  merged.set_saved_encryption(self.saved_encryption())
  merged.pdf_xobject_copy_trailer_id_from(self)
  let merged_pages = merged.pages_of_pagetree()
  guard merged_pages.length() > 0 else { raise PageTreeExpected }
  let overlay_page = merged_pages[merged_pages.length() - 1]
  let under_pages = pdf_xobject_all_but_last_page(merged_pages)
  let changed = merged.change_pages(
    under_pages,
    change_references=true,
    changes=pdf_xobject_identity_changes(under_pages.length()),
  )
  let final_page_numbers = changed.page_reference_numbers()
  let changed = if bookmarks.length() > 0 {
    changed.add_bookmarks(
      pdf_xobject_retained_bookmarks(
        bookmarks, bookmark_page_numbers, final_page_numbers,
      ),
    )
  } else {
    changed
  }
  let name = pdf_xobject_name(
    "/" + changed.shortest_unused_prefix() + "CPDFXObj",
  )
  (changed.pdf_xobject_add_page_as_xobject(range, overlay_page, name), name)
}

///|
/// Compatibility wrapper for `PdfDocument::stamp_as_xobject`.
pub fn pdf_stamp_as_xobject(
  document : PdfDocument,
  range : ArrayView[Int],
  overlay : PdfDocument,
) -> (PdfDocument, @core.PdfName) raise @core.PdfError {
  document.stamp_as_xobject(range, overlay)
}

///|
/// Compatibility wrapper matching cpdfxobject's string resource-name result.
pub fn pdf_stamp_as_xobject_string(
  document : PdfDocument,
  range : ArrayView[Int],
  overlay : PdfDocument,
) -> (PdfDocument, String) raise @core.PdfError {
  let (stamped, name) = document.stamp_as_xobject(range, overlay)
  (stamped, @ascii.decode_lossy(name.bytes()))
}