///|
fn pdf_reference_key_is_excluded(
  key : @core.PdfName,
  no_follow_entries : ArrayView[@core.PdfName],
) -> Bool {
  for excluded in no_follow_entries {
    if excluded == key {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn pdf_reference_dictionary_contains(
  entries : ArrayView[(@core.PdfName, @syntax.PdfObject)],
  marker : (@core.PdfName, @syntax.PdfObject),
) -> Bool {
  for entry in entries {
    if entry == marker {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn pdf_reference_should_follow_dictionary(
  entries : ArrayView[(@core.PdfName, @syntax.PdfObject)],
  no_follow_contains : ArrayView[(@core.PdfName, @syntax.PdfObject)],
) -> Bool {
  for marker in no_follow_contains {
    if pdf_reference_dictionary_contains(entries, marker) {
      break false
    }
  } nobreak {
    true
  }
}

///|
fn PdfDocument::collect_referenced_object_numbers(
  self : PdfDocument,
  found : @hashmap.HashMap[Int, Bool],
  object : @syntax.PdfObject,
  no_follow_entries : ArrayView[@core.PdfName],
  no_follow_contains : ArrayView[(@core.PdfName, @syntax.PdfObject)],
) -> Unit {
  match object {
    PdfIndirect(number) =>
      if number > 0 && !found.contains(number) {
        let resolved = self.lookup_object_or_null(number)
        match resolved {
          PdfDictionary(entries) =>
            if pdf_reference_should_follow_dictionary(
                entries, no_follow_contains,
              ) {
              found[number] = true
              self.collect_referenced_object_numbers(
                found, resolved, no_follow_entries, no_follow_contains,
              )
            }
          _ => {
            found[number] = true
            self.collect_referenced_object_numbers(
              found, resolved, no_follow_entries, no_follow_contains,
            )
          }
        }
      }
    PdfArray(items) =>
      for item in items {
        self.collect_referenced_object_numbers(
          found, item, no_follow_entries, no_follow_contains,
        )
      }
    PdfDictionary(entries) =>
      for entry in entries {
        if !pdf_reference_key_is_excluded(entry.0, no_follow_entries) {
          self.collect_referenced_object_numbers(
            found,
            entry.1,
            no_follow_entries,
            no_follow_contains,
          )
        }
      }
    PdfStreamObject(stream) =>
      self.collect_referenced_object_numbers(
        found,
        stream.dictionary,
        no_follow_entries,
        no_follow_contains,
      )
    _ => ()
  }
}

///|
/// Returns sorted object numbers reachable from `object`.
///
/// Indirect references are followed through the document. Dictionary keys in
/// `no_follow_entries` are skipped, and dictionaries containing any
/// `no_follow_contains` marker entry are not traversed.
pub fn PdfDocument::referenced_object_numbers(
  self : PdfDocument,
  object : @syntax.PdfObject,
  no_follow_entries? : ArrayView[@core.PdfName] = [],
  no_follow_contains? : ArrayView[(@core.PdfName, @syntax.PdfObject)] = [],
) -> Array[Int] {
  let found : @hashmap.HashMap[Int, Bool] = HashMap([])
  self.collect_referenced_object_numbers(
    found, object, no_follow_entries, no_follow_contains,
  )
  let numbers = found.keys().to_array()
  numbers.sort()
  numbers
}