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

///|
let pdf_structure_pg_key_name : PdfName = pdf_structure_name("/Pg")

///|
fn pdf_structure_pg_key() -> PdfName {
  pdf_structure_pg_key_name
}

///|
let pdf_structure_obj_key_name : PdfName = pdf_structure_name("/Obj")

///|
fn pdf_structure_obj_key() -> PdfName {
  pdf_structure_obj_key_name
}

///|
let pdf_structure_root_key_name : PdfName = pdf_structure_name("/Root")

///|
fn pdf_structure_root_key() -> PdfName {
  pdf_structure_root_key_name
}

///|
let pdf_structure_struct_tree_root_key_name : PdfName = pdf_structure_name(
  "/StructTreeRoot",
)

///|
fn pdf_structure_struct_tree_root_key() -> PdfName {
  pdf_structure_struct_tree_root_key_name
}

///|
let pdf_structure_parent_tree_key_name : PdfName = pdf_structure_name(
  "/ParentTree",
)

///|
fn pdf_structure_parent_tree_key() -> PdfName {
  pdf_structure_parent_tree_key_name
}

///|
let pdf_structure_k_key_name : PdfName = pdf_structure_name("/K")

///|
fn pdf_structure_k_key() -> PdfName {
  pdf_structure_k_key_name
}

///|
fn PdfDocument::pdf_structure_indirect_number(
  self : PdfDocument,
  object_number : Int,
  key : PdfName,
) -> Int? {
  match self.direct(PdfIndirect(object_number)).lookup_immediate(key) {
    Some(PdfIndirect(number)) => Some(number)
    _ => None
  }
}

///|
fn PdfDocument::pdf_structure_object_has_deleted_page_in_set(
  self : PdfDocument,
  object : PdfObject,
  deleted_pages : PdfNumberSet,
) -> Bool {
  match self.direct(object).lookup_immediate(pdf_structure_pg_key()) {
    Some(PdfIndirect(page_number)) => deleted_pages.contains(page_number)
    _ => false
  }
}

///|
fn PdfDocument::pdf_structure_indirect_survives_in_set(
  self : PdfDocument,
  object_number : Int,
  deleted_pages : PdfNumberSet,
) -> Bool {
  if self.lookup_object_or_null(object_number) == PdfNull {
    false
  } else {
    match
      (
        self.pdf_structure_indirect_number(
          object_number,
          pdf_structure_pg_key(),
        ),
        self.pdf_structure_indirect_number(
          object_number,
          pdf_structure_obj_key(),
        ),
      ) {
      (Some(page_number), _) if deleted_pages.contains(page_number) => false
      (_, Some(page_number)) if deleted_pages.contains(page_number) => false
      _ => true
    }
  }
}

///|
fn PdfDocument::pdf_structure_k_item_survives_in_set(
  self : PdfDocument,
  item : PdfObject,
  deleted_pages : PdfNumberSet,
) -> Bool {
  match item {
    PdfIndirect(object_number) =>
      self.pdf_structure_indirect_survives_in_set(object_number, deleted_pages)
    _ => true
  }
}

///|
fn PdfDocument::pdf_structure_filter_k_items_in_set(
  self : PdfDocument,
  items : ArrayView[PdfObject],
  deleted_pages : PdfNumberSet,
) -> Array[PdfObject]? {
  let output : Array[PdfObject] = []
  let mut changed = false
  for item in items {
    if self.pdf_structure_k_item_survives_in_set(item, deleted_pages) {
      output.push(item)
    } else {
      changed = true
    }
  }
  if changed {
    Some(output)
  } else {
    None
  }
}

///|
fn PdfDocument::pdf_structure_filtered_k_value_in_set(
  self : PdfDocument,
  object : PdfObject,
  deleted_pages : PdfNumberSet,
) -> Array[PdfObject]? {
  match self.direct(object) {
    PdfDictionary(_) as dictionary =>
      match dictionary.lookup_immediate(pdf_structure_k_key()) {
        Some(PdfInteger(_)) => None
        Some(PdfIndirect(_) as child) =>
          self.pdf_structure_filter_k_items_in_set([child], deleted_pages)
        Some(PdfArray(children)) =>
          self.pdf_structure_filter_k_items_in_set(children, deleted_pages)
        _ => None
      }
    _ => None
  }
}

///|
fn pdf_structure_k_object(items : ArrayView[PdfObject]) -> PdfObject {
  if items.length() == 1 {
    items[0]
  } else {
    PdfArray([ for item in items => item ])
  }
}

///|
fn PdfDocument::pdf_structure_parent_tree_catalog(
  self : PdfDocument,
) -> PdfObject? {
  Some(self.catalog()) catch {
    _ => None
  }
}

///|
fn PdfDocument::pdf_structure_root_parent_tree_entries(
  self : PdfDocument,
) -> Array[(Int, PdfObject)] {
  match self.pdf_structure_parent_tree_catalog() {
    Some(catalog) =>
      match self.lookup_direct(pdf_structure_struct_tree_root_key(), catalog) {
        Some(root) =>
          match self.lookup_direct(pdf_structure_parent_tree_key(), root) {
            Some(parent_tree) => self.read_number_tree(parent_tree)
            None => []
          }
        None => []
      }
    None => []
  }
}

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

///|
fn pdf_structure_apply_parent_tree_changes(
  entries : ArrayView[(Int, PdfObject)],
  changes : ArrayView[(Int, Int)],
) -> Array[(Int, PdfObject)] {
  let change_map = pdf_merge_parent_tree_change_map(changes)
  let output : Array[(Int, PdfObject)] = []
  for entry in entries {
    let (old_key, value) = entry
    match pdf_structure_changed_parent_tree_key(change_map, old_key) {
      Some(new_key) =>
        output.push(
          (new_key, pdf_merge_renumber_struct_parent_object(value, change_map)),
        )
      None => ()
    }
  }
  output
}

///|
fn PdfDocument::pdf_structure_replace_parent_tree(
  self : PdfDocument,
  parent_tree : PdfObject,
) -> Unit raise PdfError {
  match self.trailer_dict().lookup_immediate(pdf_structure_root_key()) {
    Some(root) =>
      if self.direct(root) != PdfNull {
        self.replace_chain(
          [
            pdf_structure_root_key(),
            pdf_structure_struct_tree_root_key(),
            pdf_structure_parent_tree_key(),
          ],
          parent_tree,
        )
      }
    None => {
      let catalog = self.lookup_object_or_null(self.root())
      match catalog.lookup_immediate(pdf_structure_struct_tree_root_key()) {
        Some(PdfIndirect(root_number)) => {
          let root = self.lookup_object_or_null(root_number)
          self.add_object_given_number(
            root_number,
            root.add_dict_entry(pdf_structure_parent_tree_key(), parent_tree),
          )
        }
        Some(root) =>
          self.add_object_given_number(
            self.root(),
            catalog.add_dict_entry(
              pdf_structure_struct_tree_root_key(),
              root.add_dict_entry(pdf_structure_parent_tree_key(), parent_tree),
            ),
          )
        None => ()
      }
    }
  }
}

///|
/// Renumber structure parent-tree keys across documents before merging.
///
/// The documents are mutated in place. Parent-tree keys are reassigned to a
/// single increasing sequence, and structure parent references inside each
/// document are updated to match the rebuilt number trees.
pub fn pdf_renumber_parent_trees(
  documents : ArrayView[PdfDocument],
) -> Unit raise PdfError {
  if documents.length() > 1 {
    let all_changes : Array[Array[(Int, Int)]] = []
    let all_entries : Array[Array[(Int, PdfObject)]] = []
    let mut next_key = 0
    for document in documents {
      let entries = document.pdf_structure_root_parent_tree_entries()
      let changes : Array[(Int, Int)] = []
      for entry in entries {
        let (old_key, _) = entry
        changes.push((old_key, next_key))
        next_key += 1
      }
      all_entries.push(entries)
      all_changes.push(changes)
    }
    for index in 0.. 0 {
        document.pdf_merge_renumber_struct_parent_entries(changes)
        document.pdf_structure_replace_parent_tree(
          document.build_number_tree(
            pdf_structure_apply_parent_tree_changes(all_entries[index], changes),
          ),
        )
      }
    }
  }
}

///|
fn PdfDocument::pdf_structure_trim_deleted_pages(
  self : PdfDocument,
  deleted_page_numbers : ArrayView[Int],
) -> Unit raise PdfError {
  if deleted_page_numbers.length() > 0 {
    let deleted_pages = pdf_number_set(deleted_page_numbers)
    let mut changed = true
    while changed {
      changed = false
      let deletions : Array[Int] = []
      let deletion_seen = pdf_number_set([])
      for object_number in self.object_numbers() {
        if self.pdf_structure_object_has_deleted_page_in_set(
            self.lookup_object_or_null(object_number),
            deleted_pages,
          ) {
          pdf_push_unique_number(deletions, deletion_seen, object_number)
        }
      }
      for object_number in deletions {
        if self.lookup_object_or_null(object_number) != PdfNull {
          self.remove_object(object_number)
          changed = true
        }
      }
      let replacements : Array[(Int, PdfObject)] = []
      for object_number in self.object_numbers() {
        let object = self.lookup_object_or_null(object_number)
        match
          self.pdf_structure_filtered_k_value_in_set(object, deleted_pages) {
          Some([]) =>
            pdf_push_unique_number(deletions, deletion_seen, object_number)
          Some(filtered) =>
            replacements.push(
              (
                object_number,
                object.replace_dict_entry(
                  pdf_structure_k_key(),
                  pdf_structure_k_object(filtered),
                ),
              ),
            )
          None => ()
        }
      }
      for object_number in deletions {
        if self.lookup_object_or_null(object_number) != PdfNull {
          self.remove_object(object_number)
          changed = true
        }
      }
      for replacement in replacements {
        let (object_number, object) = replacement
        if self.lookup_object_or_null(object_number) != PdfNull {
          self.add_object_given_number(object_number, object)
          changed = true
        }
      }
    }
  }
}

///|
/// Trim structure-tree entries that point to pages outside the selected pages.
///
/// `page_numbers` uses the same page-selection numbering as the page helpers.
/// The document is mutated in place by deleting structure objects tied to
/// removed pages and pruning `/K` arrays that reference them.
pub fn PdfDocument::trim_structure_tree(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Unit raise PdfError {
  let all_page_numbers = self.page_reference_numbers()
  let kept_page_numbers = pdf_page_select_numbers(
    all_page_numbers, page_numbers,
  )
  self.pdf_structure_trim_deleted_pages(
    pdf_page_deleted_numbers(all_page_numbers, kept_page_numbers),
  )
}

///|
/// Compatibility wrapper for `PdfDocument::trim_structure_tree`.
pub fn pdf_trim_structure_tree(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Unit raise PdfError {
  document.trim_structure_tree(page_numbers)
}

///|
fn PdfDocument::pdf_structure_merged_parent_tree_entries(
  self : PdfDocument,
  structure_tree_roots : ArrayView[PdfObject],
) -> Array[(Int, PdfObject)] {
  let entries : Array[(Int, PdfObject)] = []
  for root in structure_tree_roots {
    for entry in self.pdf_merge_structure_parent_tree_entries(root) {
      entries.push(entry)
    }
  }
  entries
}

///|
fn pdf_structure_tree_root_object_number(root : PdfObject) -> Int {
  match root {
    PdfIndirect(number) => number
    _ => 0
  }
}

///|
/// Merge structure tree roots from source documents into a target document.
///
/// Returns the resulting structure-tree root object number, or `None` when no
/// source document has a structure tree. When multiple roots are merged, parent
/// trees and root metadata are combined in `document`; `add_toplevel_document`
/// wraps merged children in a PDF 2.0 top-level `/Document` structure element.
pub fn pdf_merge_structure_trees(
  document : PdfDocument,
  documents : ArrayView[PdfDocument],
  add_toplevel_document? : Bool = false,
) -> Int? raise PdfError {
  let structure_tree_roots = pdf_merge_structure_tree_roots(documents)
  let has_multiple_structure_roots = structure_tree_roots.length() > 1
  let parent_tree_entries : Array[(Int, PdfObject)] = if has_multiple_structure_roots {
    document.pdf_structure_merged_parent_tree_entries(structure_tree_roots)
  } else {
    []
  }
  match
    document.pdf_merge_structure_tree_root(
      structure_tree_roots, parent_tree_entries, add_toplevel_document,
    ) {
    Some(root) => Some(pdf_structure_tree_root_object_number(root))
    None => None
  }
}