///|
priv struct PdfAnnotationJsonImportRow {
  page_number : Int
  object_number : Int
  object : @syntax.PdfObject
}

///|
priv struct PdfAnnotationJsonImportData {
  max_object_number : Int
  extras : Array[(Int, @syntax.PdfObject)]
  annotations : Array[PdfAnnotationJsonImportRow]
}

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

///|
fn pdf_annotation_json_import_data(
  json : Json,
) -> PdfAnnotationJsonImportData raise @core.PdfError {
  let entries = match json {
    Array(values) => values
    _ => raise SoftError("Bad Annotations JSON file")
  }
  let max_object_number = Ref::{ val: 0, }
  let extras : Array[(Int, @syntax.PdfObject)] = Array(
    capacity=entries.length(),
  )
  let annotations : Array[PdfAnnotationJsonImportRow] = Array(
    capacity=entries.length(),
  )
  for entry in entries {
    match entry {
      Array([number_json, object_json]) => {
        let number = pdf_annotation_json_import_number(number_json)
        if number > 0 {
          extras.push((number, pdf_object_of_json(object_json)))
          if number > max_object_number.val {
            max_object_number.val = number
          }
        }
      }
      Array([page_json, number_json, object_json]) => {
        let page_number = pdf_annotation_json_import_number(page_json)
        let object_number = pdf_annotation_json_import_number(number_json)
        if object_number <= 0 {
          raise SoftError("Bad annots entry")
        }
        annotations.push({
          page_number,
          object_number,
          object: pdf_object_of_json(object_json),
        })
        if object_number > max_object_number.val {
          max_object_number.val = object_number
        }
      }
      _ => raise SoftError("Bad annots entry")
    }
  }
  { max_object_number: max_object_number.val, extras, annotations, }
}

///|
fn PdfDocument::pdf_annotation_json_import_renumbered(
  self : PdfDocument,
  max_object_number : Int,
) -> PdfDocument raise @core.PdfError {
  if max_object_number <= 0 {
    self.copy()
  } else {
    let object_numbers = self.object_numbers()
    let changes : Array[(Int, Int)] = Array(capacity=object_numbers.length())
    for index, number in object_numbers {
      changes.push((number, max_object_number + index + 1))
    }
    self.renumber_objects(changes)
  }
}

///|
fn pdf_annotation_json_import_page_reference(
  page_reference_numbers : ArrayView[Int],
  page_number : Int,
) -> Int {
  if page_number >= 1 && page_number <= page_reference_numbers.length() {
    page_reference_numbers[page_number - 1]
  } else {
    page_number
  }
}

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

///|
fn PdfDocument::pdf_annotation_json_import_destinations(
  self : PdfDocument,
  page_reference_numbers : ArrayView[Int],
  annotation : @syntax.PdfObject,
) -> @syntax.PdfObject raise @core.PdfError {
  let mut rewritten = annotation
  match self.lookup_direct(pdf_annotation_p_key(), rewritten) {
    Some(PdfInteger(page_number)) =>
      rewritten = rewritten.add_dict_entry(
        pdf_annotation_p_key(),
        PdfIndirect(
          pdf_annotation_json_import_page_reference(
            page_reference_numbers, page_number,
          ),
        ),
      )
    _ => ()
  }
  match self.lookup_direct(pdf_annotation_dest_key(), rewritten) {
    Some(destination) =>
      rewritten = rewritten.add_dict_entry(
        pdf_annotation_dest_key(),
        pdf_annotation_json_import_destination(
          page_reference_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_import_destination(
                    page_reference_numbers, destination,
                  ),
                ),
              )
            None => ()
          }
        None => ()
      }
  }
  rewritten
}

///|
fn PdfDocument::pdf_annotation_json_import_rows_for_page(
  self : PdfDocument,
  page_reference_numbers : ArrayView[Int],
  rows : ArrayView[PdfAnnotationJsonImportRow],
  page_number : Int,
) -> Array[@syntax.PdfObject] raise @core.PdfError {
  let imported : Array[@syntax.PdfObject] = Array(capacity=rows.length())
  for row in rows {
    if row.page_number == page_number {
      self.add_object_given_number(
        row.object_number,
        self.pdf_annotation_json_import_destinations(
          page_reference_numbers,
          row.object,
        ),
      )
      imported.push(PdfIndirect(row.object_number))
    }
  }
  imported
}

///|
fn PdfDocument::pdf_annotation_json_import_pages(
  self : PdfDocument,
  rows : ArrayView[PdfAnnotationJsonImportRow],
) -> PdfDocument raise @core.PdfError {
  if rows.length() == 0 {
    self.copy()
  } else {
    let page_reference_numbers = self.page_reference_numbers()
    let pages = self.pages_of_pagetree()
    let rewritten : Array[@page.PdfPage] = Array(capacity=pages.length())
    for index, page in pages {
      let page_number = index + 1
      let imported = self.pdf_annotation_json_import_rows_for_page(
        page_reference_numbers, rows, page_number,
      )
      if imported.length() == 0 {
        rewritten.push(page)
      } else {
        let existing = match
          self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
          Some(PdfArray(annotations)) => annotations
          _ => []
        }
        rewritten.push({
          ..page,
          rest: page.rest.add_dict_entry(
            pdf_annotation_annots_key(),
            PdfArray([..existing, ..imported]),
          ),
        })
      }
    }
    self.change_pages(rewritten, change_references=true)
  }
}

///|
/// Return a copy of the document with cpdf annotation JSON appended.
///
/// Existing objects are first renumbered above the imported annotation object
/// range, matching cpdf's collision-avoidance strategy. Header rows and other
/// nonpositive extra-object rows are ignored; positive extra objects are added
/// before page annotations are rebuilt.
pub fn PdfDocument::set_annotations_json(
  self : PdfDocument,
  json : Json,
) -> PdfDocument raise @core.PdfError {
  let data = pdf_annotation_json_import_data(json)
  let document = self.pdf_annotation_json_import_renumbered(
    data.max_object_number,
  )
  for extra in data.extras {
    document.add_object_given_number(extra.0, extra.1)
  }
  document.pdf_annotation_json_import_pages(data.annotations)
}

///|
/// Compatibility wrapper for `PdfDocument::set_annotations_json`.
pub fn pdf_set_annotations_json(
  json : Json,
  document : PdfDocument,
) -> PdfDocument raise @core.PdfError {
  document.set_annotations_json(json)
}

///|
/// Compatibility wrapper matching cpdfannot's `set_annotations_json` order.
pub fn pdf_set_annotations_json_cpdf_order(
  document : PdfDocument,
  json : Json,
) -> PdfDocument raise @core.PdfError {
  document.set_annotations_json(json)
}

///|
/// Return a copy of this document with annotations copied from `source`.
pub fn PdfDocument::copy_annotations_from(
  self : PdfDocument,
  source : PdfDocument,
  page_numbers : ArrayView[Int],
) -> PdfDocument raise @core.PdfError {
  self.set_annotations_json(source.annotations_json(page_numbers))
}

///|
/// Compatibility wrapper for `PdfDocument::copy_annotations_from`.
pub fn pdf_copy_annotations(
  page_numbers : ArrayView[Int],
  source : PdfDocument,
  target : PdfDocument,
) -> PdfDocument raise @core.PdfError {
  target.copy_annotations_from(source, page_numbers)
}

///|
/// Return a copy of the document with `/Annots` removed from selected pages.
pub fn PdfDocument::remove_annotations(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> PdfDocument raise @core.PdfError {
  let pages = self.pages_of_pagetree()
  pdf_page_box_validate_page_numbers(
    page_numbers,
    pages.length(),
    "remove_annotations",
  )
  let selected_pages = @core.pdf_number_set(page_numbers)
  let rewritten : Array[@page.PdfPage] = Array(capacity=pages.length())
  for index, page in pages {
    let page_number = index + 1
    if selected_pages.contains(page_number) {
      rewritten.push({
        ..page,
        rest: page.rest.remove_dict_entry(pdf_annotation_annots_key()),
      })
    } else {
      rewritten.push(page)
    }
  }
  self.change_pages(rewritten, change_references=true)
}

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