///|
fn pdf_ua_struct_tree_header(page_object_numbers : ArrayView[Int]) -> Json {
  Json::array([
    pdf_util_json_number(0),
    Json::object(
      Map([
        ("/CPDFJSONformatversion", pdf_util_json_number(1)),
        (
          "/CPDFJSONpageobjnumbers",
          Json::array(
            [
              for number in page_object_numbers => pdf_util_json_number(number)
            ],
          ),
        ),
      ]),
    ),
  ])
}

///|
fn PdfDocument::pdf_ua_struct_tree_referenced_numbers(
  self : PdfDocument,
  struct_tree_root : @syntax.PdfObject,
) -> Array[Int] {
  self.referenced_object_numbers(struct_tree_root, no_follow_entries=[
    pdf_structure_pg_key(),
    pdf_structure_obj_key(),
    pdf_ua_stm_key(),
    pdf_ua_stm_own_key(),
  ])
}

///|
fn PdfDocument::pdf_ua_extract_struct_tree_inner(
  self : PdfDocument,
) -> Json raise @core.PdfError {
  let catalog = self.catalog() catch {
    _ => raise SoftError("extract_struct_tree: no root")
  }
  let entries : Array[Json] = [
    pdf_ua_struct_tree_header(self.page_reference_numbers()),
  ]
  match catalog.lookup_immediate(pdf_ua_struct_tree_root_key()) {
    None => ()
    Some(struct_tree_root) =>
      for number in self.pdf_ua_struct_tree_referenced_numbers(struct_tree_root) {
        entries.push(
          Json::array([
            pdf_util_json_number(number),
            self.json_of_object(
              self.lookup_object_or_null(number),
              utf8=true,
              no_stream_data=false,
            ),
          ]),
        )
      }
  }
  Json::array(entries)
}

///|
fn pdf_ua_struct_tree_json_item_number(json : Json) -> Int raise @core.PdfError {
  match json {
    Number(value, repr=_) => pdf_util_json_integral_number(value)
    _ => raise SoftError("structure 1")
  }
}

///|
fn pdf_ua_read_struct_tree_json_row(
  json : Json,
) -> (Int, @syntax.PdfObject) raise @core.PdfError {
  match json {
    Array([number_json, object_json]) =>
      (
        pdf_ua_struct_tree_json_item_number(number_json),
        pdf_object_of_json(object_json),
      )
    _ => raise SoftError("structure 1")
  }
}

///|
fn pdf_ua_rewrite_struct_tree_indirects(
  object : @syntax.PdfObject,
  replacements : @hashmap.HashMap[Int, Int],
) -> @syntax.PdfObject {
  match object {
    PdfIndirect(number) =>
      match replacements.get(number) {
        Some(new_number) => PdfIndirect(new_number)
        None => object
      }
    PdfArray(items) => {
      let output : Array[@syntax.PdfObject] = []
      for item in items {
        output.push(pdf_ua_rewrite_struct_tree_indirects(item, replacements))
      }
      PdfArray(output)
    }
    PdfDictionary(entries) => {
      let output : Array[(@core.PdfName, @syntax.PdfObject)] = []
      for entry in entries {
        output.push(
          (entry.0, pdf_ua_rewrite_struct_tree_indirects(entry.1, replacements)),
        )
      }
      PdfDictionary(output)
    }
    PdfStreamObject(stream) =>
      PdfStreamObject({
        dictionary: pdf_ua_rewrite_struct_tree_indirects(
          stream.dictionary,
          replacements,
        ),
        data: stream.data,
      })
    _ => object
  }
}

///|
fn PdfDocument::pdf_ua_replace_struct_tree_inner(
  self : PdfDocument,
  json : Json,
) -> PdfDocument raise @core.PdfError {
  let rows = match json {
    Array([header, .. rows]) => {
      let (number, _) = pdf_ua_read_struct_tree_json_row(header)
      if number == 0 {
        rows
      } else {
        raise SoftError("Top level JSON wrong. Must be list with 0 first.")
      }
    }
    _ => raise SoftError("Top level JSON wrong. Must be list with 0 first.")
  }
  let positives : Array[(Int, @syntax.PdfObject)] = []
  let new_objects : Array[(Int, @syntax.PdfObject)] = []
  for row in rows {
    let entry = pdf_ua_read_struct_tree_json_row(row)
    if entry.0 > 0 {
      positives.push(entry)
    } else {
      new_objects.push(entry)
    }
  }
  let document = self.copy()
  let next_number = document.objects.max_object_number() + 1
  let replacements : @hashmap.HashMap[Int, Int] = HashMap([])
  for index in 0.. Json raise @core.PdfError {
  self.pdf_ua_extract_struct_tree_inner()
}

///|
/// Extract the PDF/UA structure-tree JSON as UTF-8 bytes.
pub fn PdfDocument::struct_tree_json_blob(
  self : PdfDocument,
) -> @core.PdfBytes raise @core.PdfError {
  @utf8.encode(self.extract_struct_tree().stringify())
}

///|
/// Return a copy of the document with structure-tree objects replaced from JSON.
pub fn PdfDocument::replace_struct_tree(
  self : PdfDocument,
  json : Json,
) -> PdfDocument raise @core.PdfError {
  self.pdf_ua_replace_struct_tree_inner(json) catch {
    _ => raise SoftError("replace_struct_tree")
  }
}