///|
fn pdf_page_root_pages_number(pdf : PdfDocument) -> Int raise @core.PdfError {
  let root = match pdf.pdf_page_active_catalog() {
    Some((catalog, _)) => catalog
    None => raise RootExpected
  }
  match root.lookup_immediate(pdf_page_pages_key()) {
    Some(PdfIndirect(number)) => number
    _ => raise PageTreeExpected
  }
}

///|
fn PdfDocument::pdf_page_tree_nodes_not_pages_inner(
  self : PdfDocument,
  node : @syntax.PdfObject,
  node_number : Int,
  depth : Int,
) -> Array[Int] raise @core.PdfError {
  if depth > 10000 {
    raise PageTreeExpected
  }
  let nodes : Array[Int] = []
  self.pdf_page_tree_walk(
    node,
    node_number,
    depth,
    (),
    (node, number, _) => {
      match self.lookup_direct(pdf_page_type_key(), node) {
        Some(PdfNameObject(name)) if name == pdf_page_type_page() => None
        _ =>
          match self.lookup_direct(pdf_page_kids_key(), node) {
            Some(PdfArray(kids)) => {
              nodes.push(number)
              Some((pdf_page_tree_kids_number(node), kids, ()))
            }
            _ => None
          }
      }
    },
    kid => self.pdf_page_tree_indirect_kid(kid),
  )
  nodes
}

///|
/// A kid held by reference, resolved, with its number; the extract helpers
/// skip direct kids.
fn PdfDocument::pdf_page_tree_indirect_kid(
  self : PdfDocument,
  kid : @syntax.PdfObject,
) -> (@syntax.PdfObject, Int)? {
  match kid {
    PdfIndirect(number) => Some((self.direct(kid), number))
    _ => None
  }
}

///|
fn PdfDocument::pdf_page_tree_nodes_not_pages(
  self : PdfDocument,
) -> Array[Int] raise @core.PdfError {
  let pages_number = pdf_page_root_pages_number(self)
  self.pdf_page_tree_nodes_not_pages_inner(
    self.lookup_object_or_null(pages_number),
    pages_number,
    0,
  )
}

///|
fn PdfDocument::pdf_page_reference_numbers_unordered_inner(
  self : PdfDocument,
  node : @syntax.PdfObject,
  node_number : Int,
  depth : Int,
) -> Array[Int] raise @core.PdfError {
  if depth > 10000 {
    raise PageTreeExpected
  }
  let numbers : Array[Int] = []
  self.pdf_page_tree_walk(
    node,
    node_number,
    depth,
    (),
    (node, number, _) => {
      match self.lookup_direct(pdf_page_type_key(), node) {
        Some(PdfNameObject(name)) if name == pdf_page_type_page() => {
          numbers.push(number)
          None
        }
        _ =>
          match self.lookup_direct(pdf_page_kids_key(), node) {
            Some(PdfArray(kids)) =>
              Some((pdf_page_tree_kids_number(node), kids, ()))
            _ => {
              numbers.push(number)
              None
            }
          }
      }
    },
    kid => self.pdf_page_tree_indirect_kid(kid),
  )
  numbers
}

///|
fn PdfDocument::pdf_page_reference_numbers_unordered(
  self : PdfDocument,
) -> Array[Int] raise @core.PdfError {
  let pages_number = pdf_page_root_pages_number(self)
  self.pdf_page_reference_numbers_unordered_inner(
    self.lookup_object_or_null(pages_number),
    pages_number,
    0,
  )
}

///|
fn pdf_page_first_duplicate_number(numbers : ArrayView[Int]) -> Int? {
  let sorted = numbers.to_owned()
  sorted.sort()
  for i in 1.. (Array[@syntax.PdfObject], Bool) {
  let output : Array[@syntax.PdfObject] = Array(capacity=kids.length())
  let mut rewritten = false
  for kid in kids {
    match kid {
      PdfIndirect(number) if !rewritten && number == old_number => {
        output.push(PdfIndirect(new_number))
        rewritten = true
      }
      _ => output.push(kid)
    }
  }
  (output, rewritten)
}

///|
fn PdfDocument::pdf_page_rewrite_first_indirect(
  self : PdfDocument,
  node_number : Int,
  old_number : Int,
  new_number : Int,
) -> Bool raise @core.PdfError {
  let node = self.lookup_object_or_null(node_number)
  match node.lookup_immediate(pdf_page_kids_key()) {
    Some(PdfArray(kids)) => {
      let (rewritten_kids, rewritten) = pdf_page_rewrite_first_kid(
        kids, old_number, new_number,
      )
      if rewritten {
        self.add_object_given_number(
          node_number,
          node.add_dict_entry(pdf_page_kids_key(), PdfArray(rewritten_kids)),
        )
      }
      rewritten
    }
    _ => false
  }
}

///|
fn PdfDocument::pdf_page_rewrite_page_tree_first(
  self : PdfDocument,
  nodes : ArrayView[Int],
  page_number : Int,
) -> Unit raise @core.PdfError {
  let new_number = self.add_object(self.lookup_object_or_null(page_number))
  let mut rewritten = false
  for node_number in nodes {
    if !rewritten {
      rewritten = self.pdf_page_rewrite_first_indirect(
        node_number, page_number, new_number,
      )
    }
  }
  if !rewritten {
    self.remove_object(new_number)
    raise PageTreeExpected
  }
}

///|
fn PdfDocument::pdf_page_fixup_duplicate_pages(
  self : PdfDocument,
) -> Unit raise @core.PdfError {
  let nodes = self.pdf_page_tree_nodes_not_pages()
  let mut done = false
  while !done {
    match
      pdf_page_first_duplicate_number(
        self.pdf_page_reference_numbers_unordered(),
      ) {
      Some(page_number) =>
        self.pdf_page_rewrite_page_tree_first(nodes, page_number)
      None => done = true
    }
  }
}

///|
fn PdfDocument::pdf_page_fixup_parents_inner(
  self : PdfDocument,
  parent_number : Int,
  node_number : Int,
  depth : Int,
) -> Unit raise @core.PdfError {
  if depth > 10000 {
    raise PageTreeExpected
  }
  // each node is read when visited, after its earlier siblings' subtrees
  // were fixed, as the recursive walk read it
  self.pdf_page_tree_walk(
    PdfNull,
    node_number,
    depth,
    parent_number,
    (_, number, parent_number) => {
      let node = self.lookup_object_or_null(number)
      match node.lookup_immediate(pdf_page_parent_key()) {
        Some(PdfIndirect(_)) =>
          self.add_object_given_number(
            number,
            node.add_dict_entry(
              pdf_page_parent_key(),
              PdfIndirect(parent_number),
            ),
          )
        _ => ()
      }
      match self.lookup_direct(pdf_page_kids_key(), node) {
        Some(PdfArray(kids)) =>
          Some((pdf_page_tree_kids_number(node), kids, number))
        _ => None
      }
    },
    kid => {
      // read when visited (see above)
      match kid {
        PdfIndirect(number) => Some((PdfNull, number))
        _ => None
      }
    },
  )
}

///|
fn PdfDocument::pdf_page_fixup_parents(
  self : PdfDocument,
) -> Unit raise @core.PdfError {
  self.pdf_page_fixup_parents_inner(0, pdf_page_root_pages_number(self), 0)
}