///|
fn pdf_page_name_text_without_slash(name : @core.PdfName) -> String {
  let bytes = name.bytes()
  let view = if bytes.length() > 0 && bytes[0].to_int() == 47 {
    bytes[1:]
  } else {
    bytes
  }
  @ascii.decode(view) catch {
    _ => ""
  }
}

///|
fn pdf_page_string_has_prefix(text : String, prefix : String) -> Bool {
  let text_bytes = @ascii.encode(text)
  let prefix_bytes = @ascii.encode(prefix)
  if prefix_bytes.length() > text_bytes.length() {
    false
  } else {
    for i in 0.. String {
  if prefix == "" {
    "a"
  } else {
    let bytes = @ascii.encode(prefix)
    if bytes[0].to_int() == 122 {
      "a" + prefix
    } else {
      let output = bytes.to_array()
      output[0] = (output[0].to_int() + 1).to_byte()
      try! @ascii.decode(Bytes::from_array(output))
    }
  }
}

///|
fn pdf_page_prefix_is_unused(
  names : ArrayView[String],
  prefix : String,
) -> Bool {
  for name in names {
    if pdf_page_string_has_prefix(name, prefix) {
      break false
    }
  } nobreak {
    true
  }
}

///|
fn PdfDocument::pdf_page_resource_names(
  self : PdfDocument,
  resources : @syntax.PdfObject,
) -> Array[String] {
  let names : Array[String] = []
  for resource_key in pdf_page_resource_keys() {
    match self.lookup_direct(resource_key, resources) {
      Some(PdfDictionary(entries)) =>
        for entry in entries {
          let (name, _) = entry
          names.push(pdf_page_name_text_without_slash(name))
        }
      _ => ()
    }
  }
  names
}

///|
fn pdf_page_is_resource_key(key : @core.PdfName) -> Bool {
  for resource_key in pdf_page_resource_keys() {
    if key == resource_key {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn pdf_page_unknown_resource_entries(
  resources : @syntax.PdfObject,
) -> Array[(@core.PdfName, @syntax.PdfObject)] {
  let output : Array[(@core.PdfName, @syntax.PdfObject)] = []
  match resources {
    PdfDictionary(entries) =>
      for entry in entries {
        let (key, _) = entry
        if !pdf_page_is_resource_key(key) {
          output.push(entry)
        }
      }
    _ => ()
  }
  output
}

///|
fn PdfDocument::pdf_page_resource_entries(
  self : PdfDocument,
  resources : @syntax.PdfObject,
  key : @core.PdfName,
) -> Array[(@core.PdfName, @syntax.PdfObject)] {
  match self.lookup_direct(key, resources) {
    Some(PdfDictionary(entries)) => [ for entry in entries => entry ]
    _ => []
  }
}

///|
fn pdf_page_is_rest_combined_key(key : @core.PdfName) -> Bool {
  key == pdf_page_annots_key()
}

///|
fn pdf_page_unknown_rest_entries(
  rest : @syntax.PdfObject,
) -> Array[(@core.PdfName, @syntax.PdfObject)] {
  let output : Array[(@core.PdfName, @syntax.PdfObject)] = []
  match rest {
    PdfDictionary(entries) =>
      for entry in entries {
        let (key, _) = entry
        if !pdf_page_is_rest_combined_key(key) {
          output.push(entry)
        }
      }
    _ => ()
  }
  output
}

///|
fn PdfDocument::pdf_page_rest_array_values(
  self : PdfDocument,
  rest : @syntax.PdfObject,
  key : @core.PdfName,
) -> Array[@syntax.PdfObject] {
  match self.lookup_direct(key, rest) {
    Some(PdfArray(values)) => [ for value in values => value ]
    _ => []
  }
}

///|
/// Merge two page rest dictionaries for future imposition support.
///
/// Unknown entries from both dictionaries are preserved. `/Annots` arrays are
/// resolved from either dictionary and concatenated, matching the CamlPDF
/// `cpdfimpose` helper without porting the full imposition pipeline yet.
pub fn PdfDocument::combine_pdf_rests(
  self : PdfDocument,
  first : @syntax.PdfObject,
  second : @syntax.PdfObject,
) -> @syntax.PdfObject {
  let output : Array[(@core.PdfName, @syntax.PdfObject)] = []
  for entry in pdf_page_unknown_rest_entries(first) {
    output.push(entry)
  }
  for entry in pdf_page_unknown_rest_entries(second) {
    output.push(entry)
  }
  let annots = self.pdf_page_rest_array_values(first, pdf_page_annots_key())
  for annot in self.pdf_page_rest_array_values(second, pdf_page_annots_key()) {
    annots.push(annot)
  }
  if annots.length() > 0 {
    output.push((pdf_page_annots_key(), PdfArray(annots)))
  }
  PdfDictionary(output)
}

///|
/// Merge two page resource dictionaries.
///
/// Known resource categories such as `/Font`, `/XObject`, and `/ColorSpace`
/// are concatenated; unknown top-level entries from both dictionaries are
/// preserved. Later duplicate resource names are not renamed here.
pub fn PdfDocument::combine_pdf_resources(
  self : PdfDocument,
  first : @syntax.PdfObject,
  second : @syntax.PdfObject,
) -> @syntax.PdfObject {
  let output : Array[(@core.PdfName, @syntax.PdfObject)] = []
  for entry in pdf_page_unknown_resource_entries(first) {
    output.push(entry)
  }
  for entry in pdf_page_unknown_resource_entries(second) {
    output.push(entry)
  }
  for key in pdf_page_resource_keys() {
    let entries = self.pdf_page_resource_entries(first, key)
    for entry in self.pdf_page_resource_entries(second, key) {
      entries.push(entry)
    }
    if entries.length() > 0 {
      output.push((key, PdfDictionary(entries)))
    }
  }
  PdfDictionary(output)
}

///|
fn PdfDocument::pdf_page_object_uses_page_resources(
  self : PdfDocument,
  object : @syntax.PdfObject,
) -> Bool {
  match self.lookup_direct(pdf_page_type_key(), object) {
    Some(PdfNameObject(name)) =>
      name == pdf_page_type_page() || name == pdf_page_type_pages()
    _ => false
  }
}

///|
fn pdf_page_merge_dictionary_objects(
  first : @syntax.PdfObject,
  second : @syntax.PdfObject,
) -> @syntax.PdfObject raise @core.PdfError {
  let mut output = first
  match second {
    PdfDictionary(entries) =>
      for entry in entries {
        let (key, value) = entry
        output = output.add_dict_entry(key, value)
      }
    _ => raise DictionaryExpected
  }
  output
}

///|
fn PdfDocument::pdf_page_process_form_xobject(
  self : PdfDocument,
  resources : @syntax.PdfObject,
  object_number : Int,
  process : (PdfDocument, @syntax.PdfObject, ArrayView[@syntax.PdfObject]) -> Array[
    @syntax.PdfObject,
  ] raise @core.PdfError,
) -> Unit raise @core.PdfError {
  let xobject = self.lookup_object_or_null(object_number)
  match self.lookup_direct(pdf_page_subtype_key(), xobject) {
    Some(PdfNameObject(name)) if name == pdf_page_type_form() =>
      match xobject {
        PdfStreamObject(stream) => {
          xobject.get_stream()
          let processed = process(self, resources, [PdfStreamObject(stream)])
          if processed.length() != 1 {
            raise ParseStreamExpected
          }
          match processed[0] {
            PdfStreamObject(processed_stream) => {
              let dictionary = pdf_page_merge_dictionary_objects(
                stream.dictionary,
                processed_stream.dictionary,
              ).remove_dict_entry(pdf_page_filter_key())
              self.add_object_given_number(
                object_number,
                PdfStreamObject({ dictionary, data: processed_stream.data, }),
              )
            }
            _ => raise ParseStreamExpected
          }
        }
        _ => raise ParseStreamExpected
      }
    Some(_) => ()
    None => raise DictionaryKeyNotFound(pdf_page_subtype_key())
  }
}

///|
/// Adapter for callbacks that process page-like values and return identity geometry.
///
/// This preserves the CamlPDF callback shape by returning the callback result,
/// original number, and identity transform matrix.
pub fn[A, B, C] pdf_page_process_stub(
  process : (A, B) -> C raise,
  number : A,
  page : B,
) -> (C, A, @geometry.TransformMatrix) raise {
  (process(number, page), number, @geometry.transform_identity_matrix)
}

///|
/// Process Form XObjects referenced by a page resource dictionary.
///
/// For each indirect `/XObject` with subtype `/Form`, `process` receives the
/// document, page resources, and the form stream. It must return exactly one
/// processed stream, which replaces the original form stream data.
pub fn PdfDocument::process_xobjects(
  self : PdfDocument,
  page : @page.PdfPage,
  process : (PdfDocument, @syntax.PdfObject, ArrayView[@syntax.PdfObject]) -> Array[
    @syntax.PdfObject,
  ] raise @core.PdfError,
) -> Unit raise @core.PdfError {
  match self.lookup_direct(pdf_page_xobject_key(), page.resources) {
    Some(PdfDictionary(entries)) =>
      for entry in entries {
        let (_, value) = entry
        match value {
          PdfIndirect(number) =>
            self.pdf_page_process_form_xobject(page.resources, number, process)
          _ => raise DictionaryExpected
        }
      }
    _ => ()
  }
}

///|
/// Return the shortest ASCII prefix unused by page resource names.
///
/// The prefix is suitable for `add_prefix` when merging pages from independent
/// documents with potentially colliding resource names.
pub fn PdfDocument::shortest_unused_prefix(self : PdfDocument) -> String {
  let names : Array[String] = []
  for number in self.object_numbers() {
    let object = self.lookup_object_or_null(number)
    if self.pdf_page_object_uses_page_resources(object) {
      match self.lookup_direct(pdf_page_resources_key(), object) {
        Some(resources) =>
          for name in self.pdf_page_resource_names(resources) {
            names.push(name)
          }
        None => ()
      }
    }
  }
  let mut prefix = "a"
  while !pdf_page_prefix_is_unused(names, prefix) {
    prefix = pdf_page_next_prefix(prefix)
  }
  prefix
}

///|
/// Compatibility wrapper for `PdfDocument::shortest_unused_prefix`.
pub fn pdf_shortest_unused_prefix(document : PdfDocument) -> String {
  document.shortest_unused_prefix()
}