///|
fn pdf_merge_max_version(
  documents : ArrayView[PdfDocument],
) -> (Int, Int) raise PdfError {
  if documents.length() == 0 {
    raise PageTreeExpected
  }
  let mut major = documents[0].major
  let mut minor = documents[0].minor
  for i in 1.. major ||
      (candidate_major == major && candidate_minor > minor) {
      major = candidate_major
      minor = candidate_minor
    }
  }
  (major, minor)
}

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

///|
let pdf_merge_root_key_name : PdfName = pdf_merge_name("/Root")

///|
fn pdf_merge_root_key() -> PdfName {
  pdf_merge_root_key_name
}

///|
let pdf_merge_acroform_key_name : PdfName = pdf_merge_name("/AcroForm")

///|
fn pdf_merge_acroform_key() -> PdfName {
  pdf_merge_acroform_key_name
}

///|
let pdf_merge_open_action_key_name : PdfName = pdf_merge_name("/OpenAction")

///|
fn pdf_merge_open_action_key() -> PdfName {
  pdf_merge_open_action_key_name
}

///|
let pdf_merge_ocproperties_key_name : PdfName = pdf_merge_name("/OCProperties")

///|
fn pdf_merge_ocproperties_key() -> PdfName {
  pdf_merge_ocproperties_key_name
}

///|
let pdf_merge_outlines_key_name : PdfName = pdf_merge_name("/Outlines")

///|
fn pdf_merge_outlines_key() -> PdfName {
  pdf_merge_outlines_key_name
}

///|
let pdf_merge_page_labels_key_name : PdfName = pdf_merge_name("/PageLabels")

///|
fn pdf_merge_page_labels_key() -> PdfName {
  pdf_merge_page_labels_key_name
}

///|
let pdf_merge_pages_key_name : PdfName = pdf_merge_name("/Pages")

///|
fn pdf_merge_pages_key() -> PdfName {
  pdf_merge_pages_key_name
}

///|
let pdf_merge_struct_tree_root_key_name : PdfName = pdf_merge_name(
  "/StructTreeRoot",
)

///|
fn pdf_merge_struct_tree_root_key() -> PdfName {
  pdf_merge_struct_tree_root_key_name
}

///|
let pdf_merge_type_key_name : PdfName = pdf_merge_name("/Type")

///|
fn pdf_merge_type_key() -> PdfName {
  pdf_merge_type_key_name
}

///|
let pdf_merge_dests_key_name : PdfName = pdf_merge_name("/Dests")

///|
fn pdf_merge_dests_key() -> PdfName {
  pdf_merge_dests_key_name
}

///|
let pdf_merge_dest_key_name : PdfName = pdf_merge_name("/Dest")

///|
fn pdf_merge_dest_key() -> PdfName {
  pdf_merge_dest_key_name
}

///|
let pdf_merge_fields_key_name : PdfName = pdf_merge_name("/Fields")

///|
fn pdf_merge_fields_key() -> PdfName {
  pdf_merge_fields_key_name
}

///|
let pdf_merge_af_key_name : PdfName = pdf_merge_name("/AF")

///|
fn pdf_merge_af_key() -> PdfName {
  pdf_merge_af_key_name
}

///|
let pdf_merge_class_map_key_name : PdfName = pdf_merge_name("/ClassMap")

///|
fn pdf_merge_class_map_key() -> PdfName {
  pdf_merge_class_map_key_name
}

///|
let pdf_merge_id_tree_key_name : PdfName = pdf_merge_name("/IDTree")

///|
fn pdf_merge_id_tree_key() -> PdfName {
  pdf_merge_id_tree_key_name
}

///|
let pdf_merge_info_key_name : PdfName = pdf_merge_name("/Info")

///|
fn pdf_merge_info_key() -> PdfName {
  pdf_merge_info_key_name
}

///|
let pdf_merge_k_key_name : PdfName = pdf_merge_name("/K")

///|
fn pdf_merge_k_key() -> PdfName {
  pdf_merge_k_key_name
}

///|
let pdf_merge_names_key_name : PdfName = pdf_merge_name("/Names")

///|
fn pdf_merge_names_key() -> PdfName {
  pdf_merge_names_key_name
}

///|
let pdf_merge_ns_key_name : PdfName = pdf_merge_name("/NS")

///|
fn pdf_merge_ns_key() -> PdfName {
  pdf_merge_ns_key_name
}

///|
let pdf_merge_parent_key_name : PdfName = pdf_merge_name("/P")

///|
fn pdf_merge_parent_key() -> PdfName {
  pdf_merge_parent_key_name
}

///|
let pdf_merge_pronunciation_lexicon_key_name : PdfName = pdf_merge_name(
  "/PronunciationLexicon",
)

///|
fn pdf_merge_pronunciation_lexicon_key() -> PdfName {
  pdf_merge_pronunciation_lexicon_key_name
}

///|
let pdf_merge_parent_tree_key_name : PdfName = pdf_merge_name("/ParentTree")

///|
fn pdf_merge_parent_tree_key() -> PdfName {
  pdf_merge_parent_tree_key_name
}

///|
fn PdfDocument::pdf_merge_catalog(
  self : PdfDocument,
) -> PdfObject raise PdfError {
  self.catalog()
}

///|
let pdf_merge_role_map_key_name : PdfName = pdf_merge_name("/RoleMap")

///|
fn pdf_merge_role_map_key() -> PdfName {
  pdf_merge_role_map_key_name
}

///|
let pdf_merge_s_key_name : PdfName = pdf_merge_name("/S")

///|
fn pdf_merge_s_key() -> PdfName {
  pdf_merge_s_key_name
}

///|
let pdf_merge_namespaces_key_name : PdfName = pdf_merge_name("/Namespaces")

///|
fn pdf_merge_namespaces_key() -> PdfName {
  pdf_merge_namespaces_key_name
}

///|
let pdf_merge_struct_parent_key_name : PdfName = pdf_merge_name("/StructParent")

///|
fn pdf_merge_struct_parent_key() -> PdfName {
  pdf_merge_struct_parent_key_name
}

///|
let pdf_merge_struct_parents_key_name : PdfName = pdf_merge_name(
  "/StructParents",
)

///|
fn pdf_merge_struct_parents_key() -> PdfName {
  pdf_merge_struct_parents_key_name
}

///|
let pdf_merge_namedict_ap_key_name : PdfName = pdf_merge_name("/AP")

///|
let pdf_merge_namedict_javascript_key_name : PdfName = pdf_merge_name(
  "/JavaScript",
)

///|
let pdf_merge_namedict_templates_key_name : PdfName = pdf_merge_name(
  "/Templates",
)

///|
let pdf_merge_namedict_ids_key_name : PdfName = pdf_merge_name("/IDS")

///|
let pdf_merge_namedict_urls_key_name : PdfName = pdf_merge_name("/URLS")

///|
let pdf_merge_namedict_embedded_files_key_name : PdfName = pdf_merge_name(
  "/EmbeddedFiles",
)

///|
let pdf_merge_namedict_alternate_presentations_key_name : PdfName = pdf_merge_name(
  "/AlternatePresentations",
)

///|
let pdf_merge_namedict_renditions_key_name : PdfName = pdf_merge_name(
  "/Renditions",
)

///|
let pdf_merge_namedict_tree_key_names : Array[PdfName] = [
  pdf_merge_namedict_ap_key_name, pdf_merge_namedict_javascript_key_name, pdf_merge_pages_key_name,
  pdf_merge_namedict_templates_key_name, pdf_merge_namedict_ids_key_name, pdf_merge_namedict_urls_key_name,
  pdf_merge_namedict_embedded_files_key_name, pdf_merge_namedict_alternate_presentations_key_name,
  pdf_merge_namedict_renditions_key_name,
]

///|
fn pdf_merge_namedict_tree_keys() -> ArrayView[PdfName] {
  pdf_merge_namedict_tree_key_names
}

///|
let pdf_merge_document_name : PdfName = pdf_merge_name("/Document")

///|
fn PdfDocument::pdf_merge_copy_objects_from(
  self : PdfDocument,
  source : PdfDocument,
) -> Unit {
  for number in source.object_numbers() {
    let entry = source.objects.get_entry(number).unwrap()
    self.objects.set_entry(number, {
      data: entry.data,
      generation: entry.generation,
    })
  }
}

///|
priv struct PdfMergeNameSet {
  values : @hashmap.HashMap[Bytes, Bool]
}

///|
fn pdf_merge_name_set() -> PdfMergeNameSet {
  { values: HashMap([]) }
}

///|
fn PdfMergeNameSet::contains(self : PdfMergeNameSet, name : PdfName) -> Bool {
  self.values.contains(name.bytes())
}

///|
fn PdfMergeNameSet::add(self : PdfMergeNameSet, name : PdfName) -> Unit {
  self.values[name.bytes()] = true
}

///|
priv struct PdfMergeBytesSet {
  values : @hashmap.HashMap[Bytes, Bool]
}

///|
fn pdf_merge_bytes_set() -> PdfMergeBytesSet {
  { values: HashMap([]) }
}

///|
fn PdfMergeBytesSet::contains(
  self : PdfMergeBytesSet,
  bytes : PdfBytes,
) -> Bool {
  self.values.contains(bytes)
}

///|
fn PdfMergeBytesSet::add(self : PdfMergeBytesSet, bytes : PdfBytes) -> Unit {
  self.values[bytes] = true
}

///|
fn pdf_merge_catalog_extra_key(key : PdfName) -> Bool {
  !(key == pdf_merge_type_key() ||
  key == pdf_merge_pages_key() ||
  key == pdf_merge_page_labels_key() ||
  key == pdf_merge_outlines_key() ||
  key == pdf_merge_dests_key() ||
  key == pdf_merge_names_key() ||
  key == pdf_merge_ocproperties_key() ||
  key == pdf_merge_acroform_key() ||
  key == pdf_merge_struct_tree_root_key() ||
  key == pdf_merge_open_action_key())
}

///|
fn pdf_merge_catalog_extra_entries(
  documents : ArrayView[PdfDocument],
) -> Array[(PdfName, PdfObject)] raise PdfError {
  let entries : Array[(PdfName, PdfObject)] = []
  let seen = pdf_merge_name_set()
  for document in documents {
    match document.pdf_merge_catalog() {
      PdfDictionary(root_entries) =>
        for entry in root_entries {
          let (key, _) = entry
          if pdf_merge_catalog_extra_key(key) && !seen.contains(key) {
            entries.push(entry)
            seen.add(key)
          }
        }
      _ => ()
    }
  }
  entries
}

///|
fn pdf_merge_structure_tree_roots(
  documents : ArrayView[PdfDocument],
) -> Array[PdfObject] raise PdfError {
  let roots : Array[PdfObject] = []
  for document in documents {
    match
      document
      .pdf_merge_catalog()
      .lookup_immediate(pdf_merge_struct_tree_root_key()) {
      Some(root) => roots.push(root)
      None => ()
    }
  }
  roots
}

///|
fn PdfDocument::pdf_merge_structure_tree_root_k_items(
  self : PdfDocument,
  root : PdfObject,
) -> Array[PdfObject] {
  match self.direct(root).lookup_immediate(pdf_merge_k_key()) {
    Some(PdfArray(items)) => [ for item in items => item ]
    Some(item) => [item]
    None => []
  }
}

///|
fn PdfDocument::pdf_merge_structure_parent_tree_entries(
  self : PdfDocument,
  root : PdfObject,
) -> Array[(Int, PdfObject)] {
  match self.lookup_direct(pdf_merge_parent_tree_key(), root) {
    Some(parent_tree) => self.read_number_tree(parent_tree)
    None => []
  }
}

///|
fn PdfDocument::pdf_merge_structure_name_tree_entries(
  self : PdfDocument,
  structure_tree_roots : ArrayView[PdfObject],
  key : PdfName,
) -> Array[(PdfBytes, PdfObject)] {
  let entries : Array[(PdfBytes, PdfObject)] = []
  for root in structure_tree_roots {
    match self.lookup_direct(key, root) {
      Some(tree) =>
        for entry in self.read_name_tree(tree) {
          entries.push(entry)
        }
      None => ()
    }
  }
  entries
}

///|
priv struct PdfMergeDictionaryBuilder {
  entries : Array[(PdfName, PdfObject)]
  indexes : @hashmap.HashMap[Bytes, Int]
}

///|
fn pdf_merge_dictionary_builder() -> PdfMergeDictionaryBuilder {
  { entries: [], indexes: HashMap([]) }
}

///|
fn PdfMergeDictionaryBuilder::add_entry(
  self : PdfMergeDictionaryBuilder,
  key : PdfName,
  value : PdfObject,
) -> Unit {
  let index_key = key.bytes()
  match self.indexes.get(index_key) {
    Some(index) => self.entries[index] = (key, value)
    None => {
      self.indexes[index_key] = self.entries.length()
      self.entries.push((key, value))
    }
  }
}

///|
fn PdfDocument::pdf_merge_structure_dictionary(
  self : PdfDocument,
  structure_tree_roots : ArrayView[PdfObject],
  key : PdfName,
) -> PdfObject? {
  let builder = pdf_merge_dictionary_builder()
  for root in structure_tree_roots {
    match self.lookup_direct(key, root) {
      Some(PdfDictionary(root_entries)) =>
        for entry in root_entries {
          let (entry_key, value) = entry
          builder.add_entry(entry_key, value)
        }
      _ => ()
    }
  }
  if builder.entries.length() > 0 {
    Some(PdfDictionary(builder.entries))
  } else {
    None
  }
}

///|
fn pdf_merge_object_member(
  objects : ArrayView[PdfObject],
  object : PdfObject,
) -> Bool {
  for existing in objects {
    if existing == object {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn PdfDocument::pdf_merge_structure_array(
  self : PdfDocument,
  structure_tree_roots : ArrayView[PdfObject],
  key : PdfName,
) -> Array[PdfObject] {
  let values : Array[PdfObject] = []
  for root in structure_tree_roots {
    match self.lookup_direct(key, root) {
      Some(PdfArray(items)) =>
        for item in items {
          if !pdf_merge_object_member(values, item) {
            values.push(item)
          }
        }
      _ => ()
    }
  }
  values
}

///|
fn pdf_merge_parent_tree_change_map(
  changes : ArrayView[(Int, Int)],
) -> @hashmap.HashMap[Int, Int] {
  let values : @hashmap.HashMap[Int, Int] = HashMap([])
  for change in changes {
    let (old, replacement) = change
    if !values.contains(old) {
      values[old] = replacement
    }
  }
  values
}

///|
fn pdf_merge_changed_parent_tree_key(
  changes : @hashmap.HashMap[Int, Int],
  old_key : Int,
) -> Int? {
  changes.get(old_key)
}

///|
fn pdf_merge_renumber_struct_parent_object(
  object : PdfObject,
  changes : @hashmap.HashMap[Int, Int],
) -> PdfObject {
  match object {
    PdfDictionary(entries) => {
      let output : Array[(PdfName, PdfObject)] = []
      for entry in entries {
        let (key, value) = entry
        if key == pdf_merge_struct_parent_key() ||
          key == pdf_merge_struct_parents_key() {
          match value {
            PdfInteger(old_key) =>
              match pdf_merge_changed_parent_tree_key(changes, old_key) {
                Some(new_key) => output.push((key, PdfInteger(new_key)))
                None => output.push(entry)
              }
            _ =>
              output.push(
                (key, pdf_merge_renumber_struct_parent_object(value, changes)),
              )
          }
        } else {
          output.push(
            (key, pdf_merge_renumber_struct_parent_object(value, changes)),
          )
        }
      }
      PdfDictionary(output)
    }
    PdfArray(items) =>
      PdfArray(
        [
          for item in items => {
            pdf_merge_renumber_struct_parent_object(item, changes)
          }
        ],
      )
    PdfStreamObject(stream) =>
      PdfStreamObject({
        dictionary: pdf_merge_renumber_struct_parent_object(
          stream.dictionary,
          changes,
        ),
        data: stream.data,
      })
    _ => object
  }
}

///|
fn pdf_merge_renumber_struct_parent_entry(
  entry : PdfObjectEntry,
  changes : @hashmap.HashMap[Int, Int],
) -> PdfObjectEntry {
  match entry.data {
    ObjectParsed(object) =>
      {
        data: ObjectParsed(
          pdf_merge_renumber_struct_parent_object(object, changes),
        ),
        generation: entry.generation,
      }
    ObjectParsedAlreadyDecrypted(object) =>
      {
        data: ObjectParsedAlreadyDecrypted(
          pdf_merge_renumber_struct_parent_object(object, changes),
        ),
        generation: entry.generation,
      }
    _ => { data: entry.data, generation: entry.generation }
  }
}

///|
fn PdfDocument::pdf_merge_renumber_struct_parent_entries(
  self : PdfDocument,
  changes : ArrayView[(Int, Int)],
) -> Unit {
  if changes.length() > 0 {
    let change_map = pdf_merge_parent_tree_change_map(changes)
    for number in self.object_numbers() {
      let entry = self.objects.get_entry(number).unwrap()
      self.objects.set_entry(
        number,
        pdf_merge_renumber_struct_parent_entry(entry, change_map),
      )
    }
  }
}

///|
fn pdf_merge_renumber_parent_trees(
  documents : ArrayView[PdfDocument],
) -> Array[(Int, PdfObject)] raise PdfError {
  let merged_entries : Array[(Int, PdfObject)] = []
  let mut next_key = 0
  for document in documents {
    let changes : Array[(Int, Int)] = []
    for root in pdf_merge_structure_tree_roots([document]) {
      for entry in document.pdf_merge_structure_parent_tree_entries(root) {
        let (old_key, value) = entry
        changes.push((old_key, next_key))
        merged_entries.push((next_key, value))
        next_key += 1
      }
    }
    document.pdf_merge_renumber_struct_parent_entries(changes)
  }
  merged_entries
}

///|
fn PdfDocument::pdf_merge_structure_parented_item(
  self : PdfDocument,
  item : PdfObject,
  parent_number : Int,
) -> PdfObject raise PdfError {
  match item {
    PdfIndirect(number) => {
      let object = self.lookup_object_or_null(number)
      match object {
        PdfDictionary(_) | PdfStreamObject(_) =>
          self.add_object_given_number(
            number,
            object.add_dict_entry(
              pdf_merge_parent_key(),
              PdfIndirect(parent_number),
            ),
          )
        _ => ()
      }
      item
    }
    PdfDictionary(_) =>
      item.add_dict_entry(pdf_merge_parent_key(), PdfIndirect(parent_number))
    _ => item
  }
}

///|
fn PdfDocument::pdf_merge_structure_tree_root(
  self : PdfDocument,
  structure_tree_roots : ArrayView[PdfObject],
  parent_tree_entries : ArrayView[(Int, PdfObject)],
  add_toplevel_document : Bool,
) -> PdfObject? raise PdfError {
  if structure_tree_roots.length() == 0 {
    None
  } else if structure_tree_roots.length() == 1 {
    Some(structure_tree_roots[0])
  } else {
    let root_number = self.add_object(PdfNull)
    let kids : Array[PdfObject] = []
    for root in structure_tree_roots {
      for item in self.pdf_merge_structure_tree_root_k_items(root) {
        kids.push(self.pdf_merge_structure_parented_item(item, root_number))
      }
    }
    let entries : Array[(PdfName, PdfObject)] = [
      (pdf_merge_type_key(), PdfNameObject(pdf_merge_struct_tree_root_key())),
    ]
    let id_tree_entries = self.pdf_merge_structure_name_tree_entries(
      structure_tree_roots,
      pdf_merge_id_tree_key(),
    )
    if id_tree_entries.length() > 0 {
      entries.push(
        (pdf_merge_id_tree_key(), self.build_name_tree(id_tree_entries)),
      )
    }
    if parent_tree_entries.length() > 0 {
      entries.push(
        (
          pdf_merge_parent_tree_key(),
          self.build_number_tree(parent_tree_entries),
        ),
      )
    }
    match
      self.pdf_merge_structure_dictionary(
        structure_tree_roots,
        pdf_merge_role_map_key(),
      ) {
      Some(role_map) => entries.push((pdf_merge_role_map_key(), role_map))
      None => ()
    }
    match
      self.pdf_merge_structure_dictionary(
        structure_tree_roots,
        pdf_merge_class_map_key(),
      ) {
      Some(class_map) => entries.push((pdf_merge_class_map_key(), class_map))
      None => ()
    }
    let namespaces = self.pdf_merge_structure_array(
      structure_tree_roots,
      pdf_merge_namespaces_key(),
    )
    let pdf2_namespace_number = if add_toplevel_document {
      let number = self.add_object(
        PdfDictionary([
          (
            pdf_merge_ns_key(),
            PdfString(@ascii.encode("http://iso.org/pdf2/ssn")),
          ),
        ]),
      )
      namespaces.insert(0, PdfIndirect(number))
      Some(number)
    } else {
      None
    }
    if namespaces.length() > 0 {
      entries.push((pdf_merge_namespaces_key(), PdfArray(namespaces)))
    }
    let lexicon = self.pdf_merge_structure_array(
      structure_tree_roots,
      pdf_merge_pronunciation_lexicon_key(),
    )
    if lexicon.length() > 0 {
      entries.push((pdf_merge_pronunciation_lexicon_key(), PdfArray(lexicon)))
    }
    let af = self.pdf_merge_structure_array(
      structure_tree_roots,
      pdf_merge_af_key(),
    )
    if af.length() > 0 {
      entries.push((pdf_merge_af_key(), PdfArray(af)))
    }
    if kids.length() > 0 {
      let root_k = match pdf2_namespace_number {
        Some(namespace_number) => {
          let top_level_number = self.add_object(
            PdfDictionary([
              (pdf_merge_ns_key(), PdfIndirect(namespace_number)),
              (pdf_merge_s_key(), PdfNameObject(pdf_merge_document_name)),
              (pdf_merge_parent_key(), PdfIndirect(root_number)),
              (pdf_merge_k_key(), pdf_structure_k_object(kids)),
            ]),
          )
          PdfIndirect(top_level_number)
        }
        None => pdf_structure_k_object(kids)
      }
      entries.push((pdf_merge_k_key(), root_k))
    }
    self.add_object_given_number(root_number, PdfDictionary(entries))
    Some(PdfIndirect(root_number))
  }
}

///|
fn PdfDocument::pdf_merge_catalog_entry(
  self : PdfDocument,
  key : PdfName,
) -> PdfObject? raise PdfError {
  self.lookup_direct(key, self.pdf_merge_catalog())
}

///|
fn pdf_merge_acroforms(
  documents : ArrayView[PdfDocument],
) -> PdfObject? raise PdfError {
  let forms : Array[Array[(PdfName, PdfObject)]] = []
  for document in documents {
    match document.pdf_merge_catalog_entry(pdf_merge_acroform_key()) {
      Some(PdfDictionary(form_entries)) => forms.push(form_entries)
      _ => ()
    }
  }
  if forms.length() == 0 {
    None
  } else {
    let fields : Array[PdfObject] = []
    let builder = pdf_merge_dictionary_builder()
    for form_entries in forms {
      for entry in form_entries {
        let (key, value) = entry
        if key == pdf_merge_fields_key() {
          match value {
            PdfArray(items) =>
              for item in items {
                fields.push(item)
              }
            _ => ()
          }
        } else {
          builder.add_entry(key, value)
        }
      }
    }
    Some(
      PdfDictionary(builder.entries).add_dict_entry(
        pdf_merge_fields_key(),
        PdfArray(fields),
      ),
    )
  }
}

///|
fn pdf_merge_optional_content(
  documents : ArrayView[PdfDocument],
) -> PdfOptionalContent? {
  pdf_optional_content_merge(documents) catch {
    _ => None
  }
}

///|
fn pdf_merge_info_dictionary(documents : ArrayView[PdfDocument]) -> PdfObject? {
  for document in documents {
    match
      document.lookup_direct(pdf_merge_info_key(), document.trailer_dict()) {
      Some(info) => break Some(info)
      None => ()
    }
  } nobreak {
    None
  }
}

///|
fn PdfDocument::pdf_merge_open_action_destination(
  self : PdfDocument,
  open_action : PdfObject,
) -> PdfDestination raise PdfError {
  match self.direct(open_action) {
    PdfArray(_) as destination =>
      self.read_destination(destination, shallow=true)
    PdfDictionary(_) as action => DestAction(action)
    _ => DestNull
  }
}

///|
fn PdfDocument::pdf_merge_open_action_target(
  self : PdfDocument,
  open_action : PdfObject,
) -> PdfDestination raise PdfError {
  match self.direct(open_action) {
    PdfArray(_) as destination =>
      self.read_destination(destination, shallow=true)
    PdfDictionary(_) as action => self.read_destination(action, shallow=true)
    _ => DestNull
  }
}

///|
fn pdf_merge_destination_needs_named_retention(
  destination : PdfDestination,
) -> Bool {
  match destination {
    DestNamed(_) | DestString(_) => true
    _ => false
  }
}

///|
fn PdfDocument::pdf_merge_open_action(
  self : PdfDocument,
  retain_named_destinations : Bool,
) -> PdfObject? raise PdfError {
  match self.pdf_merge_catalog_entry(pdf_merge_open_action_key()) {
    Some(open_action) => {
      let target = self.pdf_merge_open_action_target(open_action)
      let destination = self.pdf_merge_open_action_destination(open_action)
      if !retain_named_destinations &&
        pdf_merge_destination_needs_named_retention(target) {
        None
      } else if self.page_number_of_target(destination) > 0 {
        Some(open_action)
      } else {
        None
      }
    }
    None => None
  }
}

///|
fn pdf_merge_open_action(
  documents : ArrayView[PdfDocument],
  retain_named_destinations : Bool,
  destination_name_changes : ArrayView[Array[(PdfBytes, PdfBytes)]],
  all_destination_name_changes : ArrayView[(PdfBytes, PdfBytes)],
  catalog_destination_name_changes : ArrayView[Array[(PdfName, PdfName)]],
  all_catalog_destination_name_changes : ArrayView[(PdfName, PdfName)],
) -> PdfObject? raise PdfError {
  for i in 0.. None
    }
    match candidate {
      Some(open_action) => {
        let name_lookup = pdf_merge_destination_name_lookup(
          destination_name_changes[i],
          all_destination_name_changes,
        )
        let catalog_name_lookup = pdf_merge_catalog_destination_name_lookup(
          catalog_destination_name_changes[i],
          all_catalog_destination_name_changes,
        )
        break Some(
          pdf_merge_rewrite_destination_callsite_object(
            open_action, name_lookup, catalog_name_lookup,
          ),
        )
      }
      None => ()
    }
  } nobreak {
    None
  }
}

///|
fn PdfDocument::pdf_merge_name_tree_entries(
  self : PdfDocument,
  tree_key : PdfName,
) -> Array[(PdfBytes, PdfObject)] raise PdfError {
  match self.pdf_merge_catalog_entry(pdf_merge_names_key()) {
    Some(names) =>
      match self.lookup_direct(tree_key, names) {
        Some(tree) => self.read_name_tree(tree)
        None => []
      }
    None => []
  }
}

///|
fn pdf_merge_name_dictionary_entries(
  documents : ArrayView[PdfDocument],
) -> Array[(PdfName, Array[(PdfBytes, PdfObject)])] raise PdfError {
  let output : Array[(PdfName, Array[(PdfBytes, PdfObject)])] = []
  for tree_key in pdf_merge_namedict_tree_keys() {
    let entries : Array[(PdfBytes, PdfObject)] = []
    for document in documents {
      for entry in document.pdf_merge_name_tree_entries(tree_key) {
        entries.push(entry)
      }
    }
    if entries.length() > 0 {
      output.push((tree_key, entries))
    }
  }
  output
}

///|
fn pdf_merge_append_ascii_suffix(name : PdfBytes, suffix : String) -> PdfBytes {
  let suffix_bytes = @ascii.encode(suffix)
  let output = Array::make(name.length() + suffix_bytes.length(), b'\x00')
  let mut position = 0
  for byte in name {
    output[position] = byte
    position += 1
  }
  for byte in suffix_bytes {
    output[position] = byte
    position += 1
  }
  Bytes::from_array(output)
}

///|
fn pdf_merge_append_name_suffix(name : PdfName, suffix : String) -> PdfName {
  pdf_name_of_bytes(pdf_merge_append_ascii_suffix(name.bytes(), suffix))
}

///|
priv struct PdfMergeDestinationNameMap {
  values : @hashmap.HashMap[Bytes, PdfBytes]
}

///|
fn pdf_merge_destination_name_map(
  changes : ArrayView[(PdfBytes, PdfBytes)],
) -> PdfMergeDestinationNameMap {
  let values : @hashmap.HashMap[Bytes, PdfBytes] = HashMap([])
  for change in changes {
    let (old_name, new_name) = change
    if !values.contains(old_name) {
      values[old_name] = new_name
    }
  }
  { values, }
}

///|
fn PdfMergeDestinationNameMap::changed(
  self : PdfMergeDestinationNameMap,
  name : PdfBytes,
) -> PdfBytes {
  match self.values.get(name) {
    Some(new_name) => new_name
    None => name
  }
}

///|
priv struct PdfMergeDestinationNameLookup {
  local_names : PdfMergeDestinationNameMap
  global_names : PdfMergeDestinationNameMap
}

///|
fn pdf_merge_destination_name_lookup(
  changes : ArrayView[(PdfBytes, PdfBytes)],
  all_changes : ArrayView[(PdfBytes, PdfBytes)],
) -> PdfMergeDestinationNameLookup {
  {
    local_names: pdf_merge_destination_name_map(changes),
    global_names: pdf_merge_destination_name_map(all_changes),
  }
}

///|
fn PdfMergeDestinationNameLookup::changed(
  self : PdfMergeDestinationNameLookup,
  name : PdfBytes,
) -> PdfBytes {
  let changed = self.local_names.changed(name)
  if changed == name {
    self.global_names.changed(name)
  } else {
    changed
  }
}

///|
priv struct PdfMergeCatalogDestinationNameMap {
  values : @hashmap.HashMap[Bytes, PdfName]
}

///|
fn pdf_merge_catalog_destination_name_map(
  changes : ArrayView[(PdfName, PdfName)],
) -> PdfMergeCatalogDestinationNameMap {
  let values : @hashmap.HashMap[Bytes, PdfName] = HashMap([])
  for change in changes {
    let (old_name, new_name) = change
    let key = old_name.bytes()
    if !values.contains(key) {
      values[key] = new_name
    }
  }
  { values, }
}

///|
fn PdfMergeCatalogDestinationNameMap::changed(
  self : PdfMergeCatalogDestinationNameMap,
  name : PdfName,
) -> PdfName {
  match self.values.get(name.bytes()) {
    Some(new_name) => new_name
    None => name
  }
}

///|
priv struct PdfMergeCatalogDestinationNameLookup {
  local_names : PdfMergeCatalogDestinationNameMap
  global_names : PdfMergeCatalogDestinationNameMap
}

///|
fn pdf_merge_catalog_destination_name_lookup(
  changes : ArrayView[(PdfName, PdfName)],
  all_changes : ArrayView[(PdfName, PdfName)],
) -> PdfMergeCatalogDestinationNameLookup {
  {
    local_names: pdf_merge_catalog_destination_name_map(changes),
    global_names: pdf_merge_catalog_destination_name_map(all_changes),
  }
}

///|
fn PdfMergeCatalogDestinationNameLookup::changed(
  self : PdfMergeCatalogDestinationNameLookup,
  name : PdfName,
) -> PdfName {
  let changed = self.local_names.changed(name)
  if changed == name {
    self.global_names.changed(name)
  } else {
    changed
  }
}

///|
fn pdf_merge_destination_name_changes_are_unique(
  changes_by_document : ArrayView[Array[(PdfBytes, PdfBytes)]],
) -> Bool {
  let seen = pdf_merge_bytes_set()
  for changes in changes_by_document {
    for change in changes {
      let (_, new_name) = change
      if seen.contains(new_name) {
        return false
      }
      seen.add(new_name)
    }
  }
  true
}

///|
fn pdf_merge_catalog_destination_name_changes_are_unique(
  changes_by_document : ArrayView[Array[(PdfName, PdfName)]],
) -> Bool {
  let seen = pdf_merge_name_set()
  for changes in changes_by_document {
    for change in changes {
      let (_, new_name) = change
      if seen.contains(new_name) {
        return false
      }
      seen.add(new_name)
    }
  }
  true
}

///|
fn pdf_merge_destination_name_lists(
  documents : ArrayView[PdfDocument],
) -> Array[Array[PdfBytes]] raise PdfError {
  let output : Array[Array[PdfBytes]] = []
  for document in documents {
    output.push(
      [
        for entry in document.pdf_merge_name_tree_entries(pdf_merge_dests_key()) => {
          entry.0
        }
      ],
    )
  }
  output
}

///|
fn PdfDocument::pdf_merge_catalog_destination_name_entries(
  self : PdfDocument,
) -> Array[PdfName] raise PdfError {
  match self.pdf_merge_catalog_entry(pdf_merge_dests_key()) {
    Some(PdfDictionary(entries)) => [ for entry in entries => entry.0 ]
    _ => []
  }
}

///|
fn pdf_merge_catalog_destination_name_lists(
  documents : ArrayView[PdfDocument],
) -> Array[Array[PdfName]] raise PdfError {
  let output : Array[Array[PdfName]] = []
  for document in documents {
    output.push(document.pdf_merge_catalog_destination_name_entries())
  }
  output
}

///|
fn pdf_merge_destination_name_changes(
  documents : ArrayView[PdfDocument],
) -> Array[Array[(PdfBytes, PdfBytes)]] raise PdfError {
  let names_by_document = pdf_merge_destination_name_lists(documents)
  let identity : Array[Array[(PdfBytes, PdfBytes)]] = [
    for names in names_by_document => [ for name in names => (name, name) ]
  ]
  if pdf_merge_destination_name_changes_are_unique(identity) {
    [
      for _ in names_by_document => []
    ]
  } else {
    let mut suffix_start = 0
    let mut changes : Array[Array[(PdfBytes, PdfBytes)]] = identity
    while !pdf_merge_destination_name_changes_are_unique(changes) {
      changes = []
      for i in 0.. Array[Array[(PdfName, PdfName)]] raise PdfError {
  let names_by_document = pdf_merge_catalog_destination_name_lists(documents)
  let identity : Array[Array[(PdfName, PdfName)]] = [
    for names in names_by_document => [ for name in names => (name, name) ]
  ]
  if pdf_merge_catalog_destination_name_changes_are_unique(identity) {
    [
      for _ in names_by_document => []
    ]
  } else {
    let mut suffix_start = 0
    let mut changes : Array[Array[(PdfName, PdfName)]] = identity
    while !pdf_merge_catalog_destination_name_changes_are_unique(changes) {
      changes = []
      for i in 0.. Array[(PdfBytes, PdfBytes)] {
  let output : Array[(PdfBytes, PdfBytes)] = []
  for document_changes in changes {
    for change in document_changes {
      output.push(change)
    }
  }
  output
}

///|
fn pdf_merge_all_catalog_destination_name_changes(
  changes : ArrayView[Array[(PdfName, PdfName)]],
) -> Array[(PdfName, PdfName)] {
  let output : Array[(PdfName, PdfName)] = []
  for document_changes in changes {
    for change in document_changes {
      output.push(change)
    }
  }
  output
}

///|
fn pdf_merge_rewrite_destination_callsite_object(
  object : PdfObject,
  name_lookup : PdfMergeDestinationNameLookup,
  catalog_name_lookup : PdfMergeCatalogDestinationNameLookup,
) -> PdfObject raise PdfError {
  match object {
    PdfArray(items) => {
      let output : Array[PdfObject] = []
      for item in items {
        output.push(
          pdf_merge_rewrite_destination_callsite_object(
            item, name_lookup, catalog_name_lookup,
          ),
        )
      }
      PdfArray(output)
    }
    PdfDictionary(entries) => {
      let output : Array[(PdfName, PdfObject)] = []
      for entry in entries {
        let (key, value) = entry
        output.push(
          (
            key,
            pdf_merge_rewrite_destination_callsite_object(
              value, name_lookup, catalog_name_lookup,
            ),
          ),
        )
      }
      let rewritten = PdfDictionary(output)
      let rewritten = match rewritten.lookup_immediate(pdf_merge_dest_key()) {
        Some(PdfString(name)) =>
          rewritten.add_dict_entry(
            pdf_merge_dest_key(),
            PdfString(name_lookup.changed(name)),
          )
        Some(PdfNameObject(name)) =>
          rewritten.add_dict_entry(
            pdf_merge_dest_key(),
            PdfNameObject(catalog_name_lookup.changed(name)),
          )
        _ => rewritten
      }
      match
        (
          rewritten.lookup_immediate(pdf_merge_s_key()),
          rewritten.lookup_immediate(pdf_dest_d_key()),
        ) {
        (Some(PdfNameObject(action)), Some(PdfString(name))) if action ==
          pdf_dest_goto_name() =>
          rewritten.add_dict_entry(
            pdf_dest_d_key(),
            PdfString(name_lookup.changed(name)),
          )
        (Some(PdfNameObject(action)), Some(PdfNameObject(name))) if action ==
          pdf_dest_goto_name() =>
          rewritten.add_dict_entry(
            pdf_dest_d_key(),
            PdfNameObject(catalog_name_lookup.changed(name)),
          )
        _ => rewritten
      }
    }
    PdfStreamObject(stream) =>
      PdfStreamObject({
        dictionary: pdf_merge_rewrite_destination_callsite_object(
          stream.dictionary,
          name_lookup,
          catalog_name_lookup,
        ),
        data: stream.data,
      })
    _ => object
  }
}

///|
fn PdfDocument::pdf_merge_rewrite_destination_callsite_names(
  self : PdfDocument,
  name_changes : ArrayView[(PdfBytes, PdfBytes)],
  all_name_changes : ArrayView[(PdfBytes, PdfBytes)],
  catalog_name_changes : ArrayView[(PdfName, PdfName)],
  all_catalog_name_changes : ArrayView[(PdfName, PdfName)],
) -> Unit raise PdfError {
  if name_changes.length() > 0 ||
    all_name_changes.length() > 0 ||
    catalog_name_changes.length() > 0 ||
    all_catalog_name_changes.length() > 0 {
    let name_lookup = pdf_merge_destination_name_lookup(
      name_changes, all_name_changes,
    )
    let catalog_name_lookup = pdf_merge_catalog_destination_name_lookup(
      catalog_name_changes, all_catalog_name_changes,
    )
    for number in self.object_numbers() {
      let object = self.lookup_object_or_null(number)
      let rewritten = pdf_merge_rewrite_destination_callsite_object(
        object, name_lookup, catalog_name_lookup,
      )
      if rewritten != object {
        self.add_object_given_number(number, rewritten)
      }
    }
    self.set_trailer_dict(
      pdf_merge_rewrite_destination_callsite_object(
        self.trailer_dict(),
        name_lookup,
        catalog_name_lookup,
      ),
    )
  }
}

///|
fn PdfDocument::pdf_merge_oldstyle_destinations(
  self : PdfDocument,
  changes : ArrayView[(Int, Int)],
  name_changes : ArrayView[(PdfName, PdfName)],
) -> Array[(PdfName, PdfObject)] raise PdfError {
  let change_map = pdf_page_reference_map(changes)
  let name_map = pdf_merge_catalog_destination_name_map(name_changes)
  match self.pdf_merge_catalog_entry(pdf_merge_dests_key()) {
    Some(PdfDictionary(entries)) => {
      let output : Array[(PdfName, PdfObject)] = []
      for entry in entries {
        let (key, destination) = entry
        let renamed_key = name_map.changed(key)
        let renamed_destination = pdf_object_of_destination(
          pdf_merge_renumber_destination_with_map(
            self.read_destination(destination, shallow=true),
            change_map,
          ),
        ) catch {
          _ => pdf_page_renumber_object_with_map(destination, change_map)
        }
        output.push((renamed_key, renamed_destination))
      }
      output
    }
    _ => []
  }
}

///|
fn PdfDocument::pdf_merge_nametree_destinations(
  self : PdfDocument,
  changes : ArrayView[(Int, Int)],
  name_changes : ArrayView[(PdfBytes, PdfBytes)],
) -> Array[(PdfBytes, PdfObject)] raise PdfError {
  let change_map = pdf_page_reference_map(changes)
  let name_map = pdf_merge_destination_name_map(name_changes)
  match self.pdf_merge_catalog_entry(pdf_merge_names_key()) {
    Some(names) =>
      match self.lookup_direct(pdf_merge_dests_key(), names) {
        Some(dests_tree) => {
          let output : Array[(PdfBytes, PdfObject)] = []
          for entry in self.read_name_tree(dests_tree) {
            let (key, destination) = entry
            let renamed_key = name_map.changed(key)
            let renamed_destination = pdf_object_of_destination(
              pdf_merge_renumber_destination_with_map(
                self.read_destination(destination, shallow=true),
                change_map,
              ),
            ) catch {
              _ => pdf_page_renumber_object_with_map(destination, change_map)
            }
            output.push((renamed_key, renamed_destination))
          }
          output
        }
        None => []
      }
    None => []
  }
}

///|
fn PdfDocument::pdf_merge_add_catalog_entry(
  self : PdfDocument,
  key : PdfName,
  value : PdfObject,
) -> PdfDocument raise PdfError {
  let root = self.pdf_merge_catalog()
  let new_root = self.add_object(root.add_dict_entry(key, value))
  self.set_root(new_root)
  self.set_trailer_dict(
    self
    .trailer_dict()
    .add_dict_entry(pdf_merge_root_key(), PdfIndirect(new_root)),
  )
  self
}

///|
fn PdfDocument::pdf_merge_add_named_destinations(
  self : PdfDocument,
  oldstyle_destinations : ArrayView[(PdfName, PdfObject)],
  nametree_destinations : ArrayView[(PdfBytes, PdfObject)],
  name_dictionary_entries : ArrayView[(PdfName, Array[(PdfBytes, PdfObject)])],
) -> PdfDocument raise PdfError {
  let pdf = if oldstyle_destinations.length() > 0 {
    self.pdf_merge_add_catalog_entry(
      pdf_merge_dests_key(),
      PdfDictionary([ for entry in oldstyle_destinations => entry ]),
    )
  } else {
    self
  }
  let names_entries : Array[(PdfName, PdfObject)] = []
  if nametree_destinations.length() > 0 {
    names_entries.push(
      (pdf_merge_dests_key(), pdf.build_name_tree(nametree_destinations)),
    )
  }
  for entry in name_dictionary_entries {
    let (tree_key, tree_entries) = entry
    names_entries.push((tree_key, pdf.build_name_tree(tree_entries)))
  }
  if names_entries.length() > 0 {
    pdf.pdf_merge_add_catalog_entry(
      pdf_merge_names_key(),
      PdfDictionary(names_entries),
    )
  } else {
    pdf
  }
}

///|
fn pdf_merge_renumber_target_page_with_map(
  target : PdfTargetPage,
  changes : PdfPageReferenceMap,
) -> PdfTargetPage {
  match target {
    TargetPageObject(number) =>
      TargetPageObject(changes.changed_reference(number))
    TargetOtherDocPageNumber(_) => target
  }
}

///|
fn pdf_merge_renumber_destination_with_map(
  destination : PdfDestination,
  changes : PdfPageReferenceMap,
) -> PdfDestination {
  match destination {
    DestAction(action) =>
      DestAction(pdf_page_renumber_object_with_map(action, changes))
    DestNull => DestNull
    DestNamed(_) => destination
    DestString(_) => destination
    DestXYZ(target, left, top, zoom) =>
      DestXYZ(
        pdf_merge_renumber_target_page_with_map(target, changes),
        left,
        top,
        zoom,
      )
    DestFit(target) =>
      DestFit(pdf_merge_renumber_target_page_with_map(target, changes))
    DestFitH(target, top) =>
      DestFitH(pdf_merge_renumber_target_page_with_map(target, changes), top)
    DestFitV(target, left) =>
      DestFitV(pdf_merge_renumber_target_page_with_map(target, changes), left)
    DestFitR(target, left, bottom, right, top) =>
      DestFitR(
        pdf_merge_renumber_target_page_with_map(target, changes),
        left,
        bottom,
        right,
        top,
      )
    DestFitB(target) =>
      DestFitB(pdf_merge_renumber_target_page_with_map(target, changes))
    DestFitBH(target, top) =>
      DestFitBH(pdf_merge_renumber_target_page_with_map(target, changes), top)
    DestFitBV(target, left) =>
      DestFitBV(pdf_merge_renumber_target_page_with_map(target, changes), left)
  }
}

///|
fn pdf_merge_renumber_bookmark_with_map(
  bookmark : PdfBookmark,
  changes : PdfPageReferenceMap,
) -> PdfBookmark {
  {
    ..bookmark,
    target: pdf_merge_renumber_destination_with_map(bookmark.target, changes),
  }
}

///|
/// Merges selected page ranges from multiple documents into one document.
///
/// Each entry in `ranges` belongs to the document at the same index and uses
/// one-based page numbers. The retain flags control whether numbering,
/// bookmarks, optional-content data, AcroForms, named destinations, name
/// dictionary entries, structure trees, info dictionaries, and catalog entries
/// are copied into the output. Raises `PdfError::PageTreeExpected` when the
/// document and range lists do not align.
pub fn pdf_merge_documents(
  documents : ArrayView[PdfDocument],
  ranges : ArrayView[Array[Int]],
  retain_numbering? : Bool = false,
  retain_bookmarks? : Bool = true,
  retain_optional_content? : Bool = true,
  retain_acroforms? : Bool = true,
  retain_named_destinations? : Bool = true,
  retain_name_dictionary? : Bool = true,
  retain_structure_tree? : Bool = true,
  add_toplevel_document? : Bool = false,
  retain_info? : Bool = true,
  retain_catalog_entries? : Bool = true,
  remove_duplicate_fonts? : Bool = false,
) -> PdfDocument raise PdfError {
  if documents.length() == 0 || documents.length() != ranges.length() {
    raise PageTreeExpected
  }
  let page_labels = if retain_numbering {
    pdf_page_label_merge(documents, ranges)
  } else {
    []
  }
  let staged : Array[PdfDocument] = []
  let mut offset = 0
  for i in 0.. []
    ]
  }
  let catalog_destination_name_changes = if retain_named_destinations {
    pdf_merge_catalog_destination_name_changes(staged)
  } else {
    [
      for _ in staged => []
    ]
  }
  let all_destination_name_changes = pdf_merge_all_destination_name_changes(
    destination_name_changes,
  )
  let all_catalog_destination_name_changes = pdf_merge_all_catalog_destination_name_changes(
    catalog_destination_name_changes,
  )
  let open_action = if retain_catalog_entries {
    pdf_merge_open_action(
      staged, retain_named_destinations, destination_name_changes, all_destination_name_changes,
      catalog_destination_name_changes, all_catalog_destination_name_changes,
    )
  } else {
    None
  }
  if retain_named_destinations {
    for i in 0.. 1 {
    pdf_merge_renumber_parent_trees(staged)
  } else {
    []
  }
  let catalog_entries = if retain_catalog_entries {
    pdf_merge_catalog_extra_entries(staged)
  } else {
    []
  }
  let merged = pdf_document_empty()
  let (major, minor) = pdf_merge_max_version(staged)
  merged.set_version(major, minor)
  let pages : Array[PdfPage] = []
  let old_page_numbers : Array[Int] = []
  let bookmarks : Array[PdfBookmark] = []
  let oldstyle_destinations : Array[(PdfName, PdfObject)] = []
  let nametree_destinations : Array[(PdfBytes, PdfObject)] = []
  for document in staged {
    merged.pdf_merge_copy_objects_from(document)
    let old_page_numbers_for_document = document.page_reference_numbers()
    for page_number in old_page_numbers_for_document {
      old_page_numbers.push(page_number)
    }
    if retain_bookmarks {
      for bookmark in document.read_bookmarks(preserve_actions=true) {
        bookmarks.push(bookmark)
      }
    }
    for page in document.pages_of_pagetree() {
      pages.push(page)
    }
  }
  let page_root_number = merged.add_pagetree(pages)
  ignore(merged.add_root(page_root_number, []))
  let merged = if catalog_entries.length() > 0 {
    for entry in catalog_entries {
      let (key, value) = entry
      ignore(merged.pdf_merge_add_catalog_entry(key, value))
    }
    merged
  } else {
    merged
  }
  let merged = match
    merged.pdf_merge_structure_tree_root(
      structure_tree_roots, parent_tree_entries, add_toplevel_document,
    ) {
    Some(structure_tree_root) =>
      merged.pdf_merge_add_catalog_entry(
        pdf_merge_struct_tree_root_key(),
        structure_tree_root,
      )
    None => merged
  }
  let new_page_numbers = merged.page_reference_numbers()
  let changes = pdf_page_reference_changes(old_page_numbers, new_page_numbers)
  let change_map = pdf_page_reference_map(changes)
  merged.pdf_page_renumber_references(changes)
  let merged = match open_action {
    Some(open_action) =>
      merged.pdf_merge_add_catalog_entry(
        pdf_merge_open_action_key(),
        pdf_page_renumber_object_with_map(open_action, change_map),
      )
    None => merged
  }
  if retain_named_destinations {
    for i in 0.. merged.write_optional_content_groups(content)
    None => merged
  }
  let merged = match acroform {
    Some(form) =>
      merged.pdf_merge_add_catalog_entry(
        pdf_merge_acroform_key(),
        PdfIndirect(merged.add_object(form)),
      )
    None => merged
  }
  let merged = if retain_named_destinations ||
    name_dictionary_entries.length() > 0 {
    merged.pdf_merge_add_named_destinations(
      oldstyle_destinations, nametree_destinations, name_dictionary_entries,
    )
  } else {
    merged
  }
  match info {
    Some(info) =>
      merged.set_trailer_dict(
        merged
        .trailer_dict()
        .add_dict_entry(
          pdf_merge_info_key(),
          PdfIndirect(merged.add_object(info)),
        ),
      )
    None => ()
  }
  merged.remove_unreferenced()
  let merged = if retain_bookmarks && bookmarks.length() > 0 {
    merged.add_bookmarks(
      [
        for bookmark in bookmarks => {
          pdf_merge_renumber_bookmark_with_map(bookmark, change_map)
        }
      ],
    )
  } else {
    merged
  }
  let merged = if retain_numbering && page_labels.length() > 0 {
    merged.write_page_labels(page_labels)
  } else {
    merged
  }
  if remove_duplicate_fonts {
    merged.remove_duplicate_fonts()
  }
  let (merged, _) = merged.with_file_id()
  merged
}

///|
/// CamlPDF-shaped convenience wrapper for merging PDFs.
///
/// `names` must have the same length as `documents`; it is retained for API
/// compatibility with CamlPDF's merge entry point. Structure-tree processing
/// and optional top-level `/Document` wrapping are controlled by the optional
/// flags.
pub fn pdf_merge_pdfs(
  retain_numbering : Bool,
  remove_duplicate_fonts : Bool,
  names : ArrayView[String],
  documents : ArrayView[PdfDocument],
  ranges : ArrayView[Array[Int]],
  process_struct_trees? : Bool = true,
  add_toplevel_document? : Bool = false,
) -> PdfDocument raise PdfError {
  if names.length() != documents.length() {
    raise PageTreeExpected
  }
  pdf_merge_documents(
    documents,
    ranges,
    retain_numbering~,
    retain_structure_tree=process_struct_trees,
    add_toplevel_document~,
    remove_duplicate_fonts~,
  )
}