///|
fn pdf_annotation_json_int_array_contains(
  values : ArrayView[Int],
  value : Int,
) -> Bool {
  for item in values {
    if item == value {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn pdf_annotation_json_rewrite_destination(
  page_numbers : PdfPageNumberLookup,
  destination : @syntax.PdfObject,
) -> @syntax.PdfObject {
  match destination {
    PdfArray(items) =>
      if items.length() == 0 {
        destination
      } else {
        match items[0] {
          PdfIndirect(number) => {
            let rewritten : Array[@syntax.PdfObject] = [
              PdfInteger(page_numbers.page_number(number)),
            ]
            for index in 1.. destination
        }
      }
    _ => destination
  }
}

///|
fn PdfDocument::pdf_annotation_json_rewrite_destinations(
  self : PdfDocument,
  page_numbers : PdfPageNumberLookup,
  annotation : @syntax.PdfObject,
) -> @syntax.PdfObject raise @core.PdfError {
  let mut rewritten = annotation
  match self.indirect_number(pdf_annotation_p_key(), rewritten) {
    Some(number) =>
      rewritten = rewritten.add_dict_entry(
        pdf_annotation_p_key(),
        PdfInteger(page_numbers.page_number(number)),
      )
    None => ()
  }
  match self.lookup_direct(pdf_annotation_dest_key(), rewritten) {
    Some(destination) =>
      rewritten = rewritten.add_dict_entry(
        pdf_annotation_dest_key(),
        pdf_annotation_json_rewrite_destination(page_numbers, destination),
      )
    None =>
      match self.lookup_direct(pdf_annotation_a_key(), rewritten) {
        Some(action) =>
          match self.lookup_direct(pdf_annotation_d_key(), action) {
            Some(destination) =>
              rewritten = rewritten.add_dict_entry(
                pdf_annotation_a_key(),
                action.add_dict_entry(
                  pdf_annotation_d_key(),
                  pdf_annotation_json_rewrite_destination(
                    page_numbers, destination,
                  ),
                ),
              )
            None => ()
          }
        None => ()
      }
  }
  rewritten
}

///|
fn pdf_annotation_json_extra_root(
  object : @syntax.PdfObject,
) -> @syntax.PdfObject raise @core.PdfError {
  match object.dictionary_view() {
    Some(_) =>
      object
      .remove_dict_entry(pdf_annotation_popup_name)
      .remove_dict_entry(pdf_annotation_parent_key())
    None => object
  }
}

///|
fn PdfDocument::pdf_annotation_json_extra_numbers(
  self : PdfDocument,
  roots : ArrayView[@syntax.PdfObject],
  annotation_numbers : ArrayView[Int],
) -> Array[Int] raise @core.PdfError {
  let found : @hashmap.HashMap[Int, Bool] = HashMap([])
  for root in roots {
    let searchable = pdf_annotation_json_extra_root(root)
    for number in self.referenced_object_numbers(searchable) {
      if !pdf_annotation_json_int_array_contains(annotation_numbers, number) {
        found[number] = true
      }
    }
  }
  let numbers = found.keys().to_array()
  numbers.sort()
  numbers
}

///|
fn PdfDocument::pdf_annotation_json_page(
  self : PdfDocument,
  page_numbers : PdfPageNumberLookup,
  page : @page.PdfPage,
  page_number : Int,
  extra_roots : Array[@syntax.PdfObject],
  annotation_numbers : Array[Int],
) -> Array[Json] raise @core.PdfError {
  match self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
    Some(PdfArray(annotations)) => {
      let output : Array[Json] = Array(capacity=annotations.length())
      for annotation_ref in annotations {
        match annotation_ref {
          PdfIndirect(object_number) => {
            let annotation = self.direct(annotation_ref)
            let rewritten = self.pdf_annotation_json_rewrite_destinations(
              page_numbers, annotation,
            )
            extra_roots.push(rewritten)
            annotation_numbers.push(object_number)
            output.push(
              Json::array([
                pdf_util_json_number(page_number),
                pdf_util_json_number(object_number),
                self.json_of_object(rewritten, utf8=true, clean_strings=true),
              ]),
            )
          }
          _ => ()
        }
      }
      output
    }
    _ => []
  }
}

///|
fn pdf_annotation_json_format_header() -> Json {
  Json::array([
    pdf_util_json_number(-1),
    Json::object(
      Map([
        (
          "/CPDFJSONannotformatversion",
          Json::object(Map([("I", pdf_util_json_number(1))])),
        ),
      ]),
    ),
  ])
}

///|
/// Return cpdf's annotation JSON export for selected pages.
///
/// Only indirect page annotation entries are exported, matching cpdf's JSON
/// path. Page-object references in `/P`, `/Dest`, and action `/D` destinations
/// without `/Dest` are rewritten to 1-based page numbers. Referenced ancillary
/// objects are appended after annotation entries, excluding `/Popup` and
/// `/Parent` links.
pub fn PdfDocument::annotations_json(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Json raise @core.PdfError {
  let pages = self.pages_of_pagetree()
  pdf_page_box_validate_page_numbers(
    page_numbers,
    pages.length(),
    "annotations_json",
  )
  let page_reference_numbers = self.page_reference_numbers()
  let page_number_lookup = pdf_page_number_lookup(page_reference_numbers)
  let selected_pages = @core.pdf_number_set(page_numbers)
  let entries : Array[Json] = Array(capacity=page_numbers.length() + 1)
  entries.push(pdf_annotation_json_format_header())
  let extra_roots : Array[@syntax.PdfObject] = Array(
    capacity=page_numbers.length(),
  )
  let annotation_numbers : Array[Int] = Array(capacity=page_numbers.length())
  for index, page in pages {
    let page_number = index + 1
    if selected_pages.contains(page_number) {
      entries.append(
        self.pdf_annotation_json_page(
          page_number_lookup, page, page_number, extra_roots, annotation_numbers,
        ),
      )
    }
  }
  for
    number in self.pdf_annotation_json_extra_numbers(
      extra_roots, annotation_numbers,
    ) {
    entries.push(
      Json::array([
        pdf_util_json_number(number),
        self.json_of_object(
          self.lookup_object_or_null(number),
          utf8=true,
          clean_strings=true,
        ),
      ]),
    )
  }
  Json::array(entries)
}

///|
/// Return cpdf's annotation JSON export as UTF-8 JSON bytes.
pub fn PdfDocument::annotations_json_blob(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> @core.PdfBytes raise @core.PdfError {
  @utf8.encode(self.annotations_json(page_numbers).stringify())
}

///|
/// Compatibility wrapper for `PdfDocument::annotations_json_blob`.
pub fn pdf_get_annotations_json(
  page_numbers : ArrayView[Int],
  document : PdfDocument,
) -> @core.PdfBytes raise @core.PdfError {
  document.annotations_json_blob(page_numbers)
}

///|
/// Compatibility wrapper matching cpdfannot's `get_annotations_json` order.
pub fn pdf_get_annotations_json_cpdf_order(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> @core.PdfBytes raise @core.PdfError {
  document.annotations_json_blob(page_numbers)
}