///|
fn pdf_renumber_document_with_changes(
  document : PdfDocument,
  changes : ArrayView[(Int, Int)],
) -> PdfDocument raise @core.PdfError {
  let output = document.copy()
  let old_numbers = output.object_numbers()
  let change_map = pdf_page_reference_map(changes)
  let entries : Array[(Int, PdfObjectEntry)] = Array(
    capacity=old_numbers.length(),
  )
  let seen : @hashmap.HashMap[Int, Bool] = HashMap([])
  for number in old_numbers {
    let new_number = change_map.changed_reference(number)
    if new_number < 1 || seen.contains(new_number) {
      raise InvalidObjectNumber(new_number)
    }
    seen[new_number] = true
    let entry = output.objects.get_entry(number).unwrap()
    entries.push(
      (new_number, pdf_page_renumber_entry_with_map(entry, change_map)),
    )
  }
  let objects = pdf_objects_of_entries(entries)
  for number in output.objects.object_stream_ids.keys() {
    if output.objects.object_map.contains(number) {
      let new_number = change_map.changed_reference(number)
      objects.object_stream_ids[new_number] = output.objects.object_stream_ids
        .get(number)
        .unwrap()
    }
  }
  {
    ..output,
    root: change_map.changed_reference(output.root()),
    objects,
    trailer_dict: pdf_page_renumber_object_with_map(
      output.trailer_dict(),
      change_map,
    ),
  }
}

///|
/// Return a copy of the document with object numbers remapped.
///
/// `changes` maps old object numbers to new object numbers; omitted objects keep
/// their original number. All indirect references, the root reference, trailer
/// dictionary, and object-stream metadata are updated. Duplicate or nonpositive
/// target numbers raise `@core.PdfError::InvalidObjectNumber`.
pub fn PdfDocument::renumber_objects(
  self : PdfDocument,
  changes : ArrayView[(Int, Int)],
) -> PdfDocument raise @core.PdfError {
  pdf_renumber_document_with_changes(self, changes)
}

///|
/// Return a copy whose objects are numbered compactly from 1 in object order.
pub fn PdfDocument::renumber_objects_compact(
  self : PdfDocument,
) -> PdfDocument raise @core.PdfError {
  let numbers = self.object_numbers()
  let changes : Array[(Int, Int)] = Array(capacity=numbers.length())
  for index, number in numbers {
    changes.push((number, index + 1))
  }
  pdf_renumber_document_with_changes(self, changes)
}

///|
/// Return a copy with every object number increased by `offset`.
///
/// `offset == 0` returns a plain copy. Negative offsets raise
/// `@core.PdfError::InvalidObjectNumber`.
pub fn PdfDocument::renumber_objects_by_offset(
  self : PdfDocument,
  offset : Int,
) -> PdfDocument raise @core.PdfError {
  if offset < 0 {
    raise InvalidObjectNumber(offset)
  }
  if offset == 0 {
    self.copy()
  } else {
    let old_numbers = self.object_numbers()
    let changes : Array[(Int, Int)] = [
      for number in old_numbers => (number, number + offset)
    ]
    pdf_renumber_document_with_changes(self, changes)
  }
}

///|
/// Return copies of documents renumbered into one non-overlapping sequence.
///
/// The first document starts at object number 1; each following document starts
/// after the previous document's object count. This is primarily used before
/// merging documents that may have colliding object numbers.
pub fn pdf_renumber_documents(
  documents : ArrayView[PdfDocument],
) -> Array[PdfDocument] raise @core.PdfError {
  let output : Array[PdfDocument] = Array(capacity=documents.length())
  let mut base = 1
  for document in documents {
    let numbers = document.object_numbers()
    let changes : Array[(Int, Int)] = Array(capacity=numbers.length())
    for index, number in numbers {
      changes.push((number, base + index))
    }
    base += numbers.length()
    output.push(pdf_renumber_document_with_changes(document, changes))
  }
  output
}