///|
priv struct PdfPageMatrixLookup {
  page_numbers : PdfPageNumberLookup
  matrices : @hashmap.HashMap[Int, TransformMatrix]
}

///|
fn pdf_page_matrix_lookup(
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
) -> PdfPageMatrixLookup {
  let values : @hashmap.HashMap[Int, TransformMatrix] = HashMap([])
  for entry in matrices {
    let (matrix_page_number, matrix) = entry
    if !values.contains(matrix_page_number) {
      values[matrix_page_number] = matrix
    }
  }
  {
    page_numbers: pdf_page_number_lookup(page_reference_numbers),
    matrices: values,
  }
}

///|
fn PdfPageMatrixLookup::matrix_for_page_number(
  self : PdfPageMatrixLookup,
  page_number : Int,
) -> TransformMatrix? {
  self.matrices.get(page_number)
}

///|
fn PdfPageMatrixLookup::page_number_of_target(
  self : PdfPageMatrixLookup,
  target : PdfTargetPage,
) -> Int {
  match target {
    TargetPageObject(object_number) =>
      self.page_numbers.page_number(object_number)
    TargetOtherDocPageNumber(_) => 0
  }
}

///|
fn PdfDocument::pdf_page_matrix_for_destination_with_lookup(
  self : PdfDocument,
  destination : PdfDestination,
  lookup : PdfPageMatrixLookup,
) -> TransformMatrix? raise PdfError {
  let page_number = self.pdf_page_matrix_page_number_for_destination_with_lookup(
    destination, lookup,
  )
  if page_number == 0 {
    None
  } else {
    lookup.matrix_for_page_number(page_number)
  }
}

///|
fn PdfDocument::pdf_page_matrix_page_number_for_destination_with_lookup(
  self : PdfDocument,
  destination : PdfDestination,
  lookup : PdfPageMatrixLookup,
) -> Int raise PdfError {
  match destination {
    DestNamed(_) | DestString(_) => {
      let resolved = self.read_destination(
        pdf_object_of_destination(destination),
      )
      if resolved == destination {
        0
      } else {
        self.pdf_page_matrix_page_number_for_destination_with_lookup(
          resolved, lookup,
        )
      }
    }
    DestAction(action) =>
      match
        (
          self.lookup_direct(pdf_dest_s_key(), action),
          self.lookup_direct(pdf_dest_d_key(), action),
        ) {
        (Some(PdfNameObject(name)), Some(raw_destination)) if name ==
          pdf_dest_goto_name() =>
          self.pdf_page_matrix_page_number_for_destination_with_lookup(
            self.read_destination(raw_destination),
            lookup,
          )
        _ => 0
      }
    DestXYZ(target, _, _, _)
    | DestFitH(target, _)
    | DestFitV(target, _)
    | DestFitR(target, _, _, _, _)
    | DestFitBH(target, _)
    | DestFitBV(target, _) => lookup.page_number_of_target(target)
    _ => 0
  }
}

///|
fn PdfDocument::pdf_page_transform_destination_object_with_lookup(
  self : PdfDocument,
  raw_destination : PdfObject,
  lookup : PdfPageMatrixLookup,
) -> PdfObject? raise PdfError {
  match
    self.pdf_page_transform_destination_object_with_shallow_lookup(
      raw_destination, lookup, false,
    ) {
    Some(destination) => Some(PdfIndirect(self.add_object(destination)))
    None => None
  }
}

///|
fn PdfDocument::pdf_page_transform_destination_object_with_shallow_lookup(
  self : PdfDocument,
  raw_destination : PdfObject,
  lookup : PdfPageMatrixLookup,
  shallow : Bool,
) -> PdfObject? raise PdfError {
  let destination = self.read_destination(raw_destination, shallow~)
  match self.pdf_page_matrix_for_destination_with_lookup(destination, lookup) {
    Some(matrix) if matrix != transform_identity_matrix =>
      Some(
        pdf_object_of_destination(
          self.transform_destination(matrix, destination),
        ),
      )
    _ => None
  }
}

///|
fn PdfDocument::pdf_page_transform_catalog_destinations_object(
  self : PdfDocument,
  dests : PdfObject,
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
) -> PdfObject? {
  let lookup = pdf_page_matrix_lookup(matrices, page_reference_numbers)
  match dests {
    PdfDictionary(entries) => {
      let output : Array[(PdfName, PdfObject)] = Array::new(
        capacity=entries.length(),
      )
      let mut changed = false
      for entry in entries {
        let (key, destination) = entry
        try {
          match
            self.pdf_page_transform_destination_object_with_shallow_lookup(
              destination, lookup, true,
            ) {
            Some(transformed) => {
              output.push((key, transformed))
              changed = true
            }
            None => output.push(entry)
          }
        } catch {
          _ => output.push(entry)
        }
      }
      if changed {
        Some(PdfDictionary(output))
      } else {
        None
      }
    }
    _ => None
  }
}

///|
fn PdfDocument::pdf_page_active_catalog(
  self : PdfDocument,
) -> (PdfObject, Bool)? {
  let uses_trailer_root = self
    .trailer_dict()
    .lookup_immediate(pdf_page_root_key())
    is Some(_)
  Some((self.catalog(), uses_trailer_root)) catch {
    _ => None
  }
}

///|
fn PdfDocument::pdf_page_replace_active_catalog_entry(
  self : PdfDocument,
  key : PdfName,
  value : PdfObject,
  catalog : PdfObject,
  uses_trailer_root : Bool,
) -> Unit raise PdfError {
  if uses_trailer_root {
    self.replace_chain([pdf_page_root_key(), key], value)
  } else {
    self.add_object_given_number(
      self.root(),
      catalog.add_dict_entry(key, value),
    )
  }
}

///|
fn PdfDocument::pdf_page_transform_catalog_destinations_for_matrices(
  self : PdfDocument,
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
) -> Unit raise PdfError {
  match self.pdf_page_active_catalog() {
    Some((root_object, uses_trailer_root)) =>
      match root_object.lookup_immediate(pdf_page_dests_key()) {
        Some(PdfIndirect(dests_number)) =>
          match
            self.pdf_page_transform_catalog_destinations_object(
              self.lookup_object_or_null(dests_number),
              matrices,
              page_reference_numbers,
            ) {
            Some(dests) => self.add_object_given_number(dests_number, dests)
            None => ()
          }
        Some(dests) =>
          match
            self.pdf_page_transform_catalog_destinations_object(
              dests, matrices, page_reference_numbers,
            ) {
            Some(transformed) =>
              self.pdf_page_replace_active_catalog_entry(
                pdf_page_dests_key(),
                transformed,
                root_object,
                uses_trailer_root,
              )
            None => ()
          }
        None => ()
      }
    None => ()
  }
}

///|
fn PdfDocument::pdf_page_transform_name_tree_destination_entries(
  self : PdfDocument,
  dests_tree : PdfObject,
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
) -> Array[(PdfBytes, PdfObject)]? {
  let lookup = pdf_page_matrix_lookup(matrices, page_reference_numbers)
  let entries = self.read_name_tree(dests_tree)
  let output : Array[(PdfBytes, PdfObject)] = Array::new(
    capacity=entries.length(),
  )
  let mut changed = false
  for entry in entries {
    let (key, destination) = entry
    try {
      match
        self.pdf_page_transform_destination_object_with_shallow_lookup(
          destination, lookup, true,
        ) {
        Some(transformed) => {
          output.push((key, transformed))
          changed = true
        }
        None => output.push(entry)
      }
    } catch {
      _ => output.push(entry)
    }
  }
  if changed {
    Some(output)
  } else {
    None
  }
}

///|
fn PdfDocument::pdf_page_transform_destination_name_tree_for_matrices(
  self : PdfDocument,
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
) -> Unit raise PdfError {
  match self.pdf_page_active_catalog() {
    Some((root_object, uses_trailer_root)) =>
      match root_object.lookup_immediate(pdf_page_names_key()) {
        Some(PdfIndirect(names_number)) => {
          let names_object = self.lookup_object_or_null(names_number)
          match self.lookup_direct(pdf_page_dests_key(), names_object) {
            Some(dests_tree) =>
              match
                self.pdf_page_transform_name_tree_destination_entries(
                  dests_tree, matrices, page_reference_numbers,
                ) {
                Some(entries) =>
                  self.add_object_given_number(
                    names_number,
                    names_object.add_dict_entry(
                      pdf_page_dests_key(),
                      self.build_name_tree(entries),
                    ),
                  )
                None => ()
              }
            None => ()
          }
        }
        Some(PdfDictionary(_) as names_object) =>
          match self.lookup_direct(pdf_page_dests_key(), names_object) {
            Some(dests_tree) =>
              match
                self.pdf_page_transform_name_tree_destination_entries(
                  dests_tree, matrices, page_reference_numbers,
                ) {
                Some(entries) =>
                  self.pdf_page_replace_active_catalog_entry(
                    pdf_page_names_key(),
                    names_object.add_dict_entry(
                      pdf_page_dests_key(),
                      self.build_name_tree(entries),
                    ),
                    root_object,
                    uses_trailer_root,
                  )
                None => ()
              }
            None => ()
          }
        _ => ()
      }
    _ => ()
  }
}

///|
fn PdfDocument::pdf_page_transform_action_object_with_lookup(
  self : PdfDocument,
  raw_action : PdfObject,
  lookup : PdfPageMatrixLookup,
) -> PdfObject? raise PdfError {
  let action = self.direct(raw_action)
  match
    (
      self.lookup_direct(pdf_dest_s_key(), action),
      action.lookup_immediate(pdf_dest_d_key()),
    ) {
    (Some(PdfNameObject(name)), Some(raw_destination)) if name ==
      pdf_dest_goto_name() =>
      match
        self.pdf_page_transform_destination_object_with_lookup(
          raw_destination, lookup,
        ) {
        Some(destination) =>
          Some(action.add_dict_entry(pdf_dest_d_key(), destination))
        None => None
      }
    _ => None
  }
}

///|
fn PdfDocument::pdf_page_transform_bookmarks_for_matrices(
  self : PdfDocument,
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
  let bookmarks = self.read_bookmarks(preserve_actions=true)
  let lookup = pdf_page_matrix_lookup(matrices, page_reference_numbers)
  let output : Array[PdfBookmark] = Array::new(capacity=bookmarks.length())
  let mut changed = false
  for bookmark in bookmarks {
    let target = match
      self.pdf_page_matrix_for_destination_with_lookup(bookmark.target, lookup) {
      Some(matrix) if matrix != transform_identity_matrix => {
        changed = true
        self.transform_destination(matrix, bookmark.target)
      }
      _ => bookmark.target
    }
    output.push({ ..bookmark, target, })
  }
  if changed {
    self.add_bookmarks(output)
  } else {
    self
  }
}

///|
fn PdfDocument::pdf_page_transform_link_annotation_object_with_lookup(
  self : PdfDocument,
  annotation : PdfObject,
  lookup : PdfPageMatrixLookup,
) -> PdfObject? raise PdfError {
  match self.lookup_direct(pdf_page_subtype_key(), annotation) {
    Some(PdfNameObject(kind)) if kind == pdf_page_type_link() =>
      match annotation.lookup_immediate(pdf_page_dest_key()) {
        Some(raw_destination) =>
          match
            self.pdf_page_transform_destination_object_with_lookup(
              raw_destination, lookup,
            ) {
            Some(destination) =>
              Some(annotation.add_dict_entry(pdf_page_dest_key(), destination))
            None => None
          }
        None =>
          match annotation.lookup_immediate(pdf_page_action_key()) {
            Some(raw_action) =>
              match
                self.pdf_page_transform_action_object_with_lookup(
                  raw_action, lookup,
                ) {
                Some(action) =>
                  Some(annotation.add_dict_entry(pdf_page_action_key(), action))
                None => None
              }
            None => None
          }
      }
    _ => None
  }
}

///|
fn PdfDocument::pdf_page_transform_link_annotations_for_matrices(
  self : PdfDocument,
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
) -> Unit raise PdfError {
  let lookup = pdf_page_matrix_lookup(matrices, page_reference_numbers)
  for page_number in self.page_reference_numbers() {
    let page_object = self.lookup_object_or_null(page_number)
    match self.lookup_direct(pdf_page_annots_key(), page_object) {
      Some(PdfArray(annotations)) =>
        for index in 0..
              try {
                let annotation = self.lookup_object_or_null(annotation_number)
                match
                  self.pdf_page_transform_link_annotation_object_with_lookup(
                    annotation, lookup,
                  ) {
                  Some(updated) =>
                    self.add_object_given_number(annotation_number, updated)
                  None => ()
                }
              } catch {
                _ => ()
              }
            PdfDictionary(_) as annotation =>
              try {
                match
                  self.pdf_page_transform_link_annotation_object_with_lookup(
                    annotation, lookup,
                  ) {
                  Some(updated) => annotations[index] = updated
                  None => ()
                }
              } catch {
                _ => ()
              }
            _ => ()
          }
        }
      _ => ()
    }
  }
}

///|
fn PdfDocument::pdf_page_transform_open_action_for_matrices(
  self : PdfDocument,
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
) -> Unit {
  let lookup = pdf_page_matrix_lookup(matrices, page_reference_numbers)
  match self.pdf_page_active_catalog() {
    Some((root_object, uses_trailer_root)) =>
      match root_object.lookup_immediate(pdf_page_open_action_key()) {
        Some(raw_open_action) =>
          match self.direct(raw_open_action) {
            PdfArray(_) as raw_destination =>
              try {
                match
                  self.pdf_page_transform_destination_object_with_lookup(
                    raw_destination, lookup,
                  ) {
                  Some(destination) =>
                    self.pdf_page_replace_active_catalog_entry(
                      pdf_page_open_action_key(),
                      destination,
                      root_object,
                      uses_trailer_root,
                    )
                  None => ()
                }
              } catch {
                _ => ()
              }
            PdfDictionary(_) as raw_action =>
              try {
                match
                  self.pdf_page_transform_action_object_with_lookup(
                    raw_action, lookup,
                  ) {
                  Some(action) =>
                    self.pdf_page_replace_active_catalog_entry(
                      pdf_page_open_action_key(),
                      action,
                      root_object,
                      uses_trailer_root,
                    )
                  None => ()
                }
              } catch {
                _ => ()
              }
            _ => ()
          }
        None => ()
      }
    None => ()
  }
}

///|
fn PdfDocument::pdf_page_transform_matrix_destinations(
  self : PdfDocument,
  matrices : ArrayView[(Int, TransformMatrix)],
  page_reference_numbers : ArrayView[Int],
  transform_bookmarks : Bool,
) -> PdfDocument raise PdfError {
  let pdf = if transform_bookmarks {
    self.pdf_page_transform_bookmarks_for_matrices(
      matrices, page_reference_numbers,
    )
  } else {
    self
  }
  pdf.pdf_page_transform_link_annotations_for_matrices(
    matrices, page_reference_numbers,
  )
  pdf.pdf_page_transform_open_action_for_matrices(
    matrices, page_reference_numbers,
  )
  pdf.pdf_page_transform_catalog_destinations_for_matrices(
    matrices, page_reference_numbers,
  )
  pdf.pdf_page_transform_destination_name_tree_for_matrices(
    matrices, page_reference_numbers,
  )
  pdf
}

///|
/// Replace the document page tree with `pages`.
///
/// When `change_references` is true, page-object references in destinations,
/// actions, annotations, and bookmarks are rewritten from the old page set to
/// the new one. `matrices` can transform destination coordinates for pages
/// whose geometry changed, and `changes` can map old page serials to new
/// serials when page counts differ.
pub fn PdfDocument::change_pages(
  self : PdfDocument,
  pages : ArrayView[PdfPage],
  change_references? : Bool = false,
  matrices? : ArrayView[(Int, TransformMatrix)] = [],
  changes? : ArrayView[(Int, Int)] = [],
) -> PdfDocument raise PdfError {
  let old_page_numbers = self.page_reference_numbers()
  let pdf = self.copy()
  let page_root_number = pdf.add_pagetree(pages)
  ignore(pdf.add_root(page_root_number, []))
  if change_references {
    let new_page_numbers = pdf.page_reference_numbers()
    let reference_changes = if changes.length() > 0 {
      pdf_page_reference_changes_from_serials(
        old_page_numbers, new_page_numbers, changes,
      )
    } else if old_page_numbers.length() != new_page_numbers.length() {
      []
    } else {
      pdf_page_reference_changes(old_page_numbers, new_page_numbers)
    }
    pdf.pdf_page_renumber_references(reference_changes)
    if matrices.length() > 0 {
      pdf.pdf_page_transform_matrix_destinations(
        matrices,
        new_page_numbers,
        old_page_numbers.length() == new_page_numbers.length(),
      )
    } else {
      pdf
    }
  } else {
    pdf
  }
}

///|
/// Compatibility wrapper for `PdfDocument::change_pages`.
pub fn pdf_change_pages(
  change_references : Bool,
  document : PdfDocument,
  pages : ArrayView[PdfPage],
  matrices? : ArrayView[(Int, TransformMatrix)] = [],
  changes? : ArrayView[(Int, Int)] = [],
) -> PdfDocument raise PdfError {
  document.change_pages(pages, change_references~, matrices~, changes~)
}