///|
fn pdf_annotation_rectangle_object(
  rectangle : @geometry.PdfRectangle,
) -> @syntax.PdfObject {
  PdfArray([
    PdfReal(rectangle.min_x),
    PdfReal(rectangle.min_y),
    PdfReal(rectangle.max_x),
    PdfReal(rectangle.max_y),
  ])
}

///|
fn pdf_annotation_border_object(
  border : @annotation.PdfAnnotationBorder,
) -> @syntax.PdfObject {
  let base : Array[@syntax.PdfObject] = [
    PdfReal(border.horizontal_radius),
    PdfReal(border.vertical_radius),
    PdfReal(border.width),
  ]
  if border.dash_array.length() > 0 {
    base.push(PdfArray([ for dash in border.dash_array => PdfInteger(dash) ]))
  }
  PdfArray(base)
}

///|
fn pdf_annotation_rest_entries(
  rest : @syntax.PdfObject,
) -> Array[(@core.PdfName, @syntax.PdfObject)] raise @core.PdfError {
  match rest {
    PdfNull => []
    PdfDictionary(entries) => [ for entry in entries => entry ]
    _ => raise DictionaryExpected
  }
}

///|
fn pdf_object_of_annotation(
  annotation : @annotation.PdfAnnotation,
) -> @syntax.PdfObject raise @core.PdfError {
  let entries : Array[(@core.PdfName, @syntax.PdfObject)] = [
    (
      pdf_annotation_subtype_key(),
      PdfNameObject(pdf_annotation_subtype_name(annotation.subtype)),
    ),
    (
      pdf_annotation_contents_key(),
      match annotation.contents {
        Some(contents) => PdfString(contents)
        None => PdfNull
      },
    ),
    (
      pdf_annotation_rect_key(),
      pdf_annotation_rectangle_object(annotation.rectangle),
    ),
    (
      pdf_annotation_border_key(),
      pdf_annotation_border_object(annotation.border),
    ),
  ]
  for entry in pdf_annotation_rest_entries(annotation.rest) {
    entries.push(entry)
  }
  match annotation.colour {
    Some(colour) =>
      entries.push(
        (
          pdf_annotation_colour_key(),
          PdfArray([
            PdfInteger(colour.red),
            PdfInteger(colour.green),
            PdfInteger(colour.blue),
          ]),
        ),
      )
    None => ()
  }
  match annotation.subject {
    Some(subject) =>
      entries.push((pdf_annotation_subj_key(), PdfString(subject)))
    None => ()
  }
  PdfDictionary(entries)
}

///|
/// Return a page value with an annotation prepended to `/Annots`.
///
/// The annotation is written as a direct dictionary. Existing annotation arrays
/// are preserved after the new entry; a non-array `/Annots` value raises.
pub fn PdfDocument::add_annotation(
  self : PdfDocument,
  page : @page.PdfPage,
  annotation : @annotation.PdfAnnotation,
) -> @page.PdfPage raise @core.PdfError {
  let object = pdf_object_of_annotation(annotation)
  let annotations = match
    self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
    Some(PdfArray(existing)) => [object, ..existing]
    Some(_) => raise DictionaryExpected
    None => [object]
  }
  {
    ..page,
    rest: page.rest.add_dict_entry(
      pdf_annotation_annots_key(),
      PdfArray(annotations),
    ),
  }
}

///|
/// Compatibility wrapper for `PdfDocument::add_annotation`.
pub fn pdf_add_annotation(
  document : PdfDocument,
  page : @page.PdfPage,
  annotation : @annotation.PdfAnnotation,
) -> @page.PdfPage raise @core.PdfError {
  document.add_annotation(page, annotation)
}

///|
fn PdfDocument::pdf_annotation_transform_one(
  self : PdfDocument,
  matrix : @geometry.TransformMatrix,
  annotation_number : Int,
) -> Unit raise @core.PdfError {
  let annotation = self.lookup_object_or_null(annotation_number)
  let transformed = self.pdf_annotation_transform_object(matrix, annotation)
  if transformed != annotation {
    self.add_object_given_number(annotation_number, transformed)
  }
}

///|
fn PdfDocument::pdf_annotation_transform_object(
  self : PdfDocument,
  matrix : @geometry.TransformMatrix,
  annotation : @syntax.PdfObject,
) -> @syntax.PdfObject raise @core.PdfError {
  let mut transformed = annotation
  match self.lookup_direct(pdf_annotation_rect_key(), annotation) {
    Some(rectangle) =>
      transformed = transformed.add_dict_entry(
        pdf_annotation_rect_key(),
        self.transform_rect(matrix, rectangle),
      )
    None => ()
  }
  match self.lookup_direct(pdf_annotation_quadpoints_key(), annotation) {
    Some(quadpoints) =>
      transformed = transformed.add_dict_entry(
        pdf_annotation_quadpoints_key(),
        self.transform_quadpoints(matrix, quadpoints),
      )
    None => ()
  }
  match self.lookup_direct(pdf_annotation_line_key(), annotation) {
    Some(line) =>
      transformed = transformed.add_dict_entry(
        pdf_annotation_line_key(),
        self.transform_rect(matrix, line),
      )
    None => ()
  }
  transformed
}

///|
/// Transform annotation geometry referenced by a page dictionary.
///
/// Applies `matrix` to `/Rect`, `/QuadPoints`, and `/L` entries. Indirect
/// annotations are updated in this document; direct annotation dictionaries in
/// the page's `/Annots` array are updated in place.
pub fn PdfDocument::transform_annotations(
  self : PdfDocument,
  matrix : @geometry.TransformMatrix,
  rest : @syntax.PdfObject,
) -> Unit raise @core.PdfError {
  match self.lookup_direct(pdf_annotation_annots_key(), rest) {
    Some(PdfArray(annotations)) =>
      for index in 0..
            self.pdf_annotation_transform_one(matrix, number)
          PdfDictionary(_) as annotation => {
            let transformed = self.pdf_annotation_transform_object(
              matrix, annotation,
            )
            if transformed != annotation {
              annotations[index] = transformed
            }
          }
          _ => ()
        }
      }
    _ => ()
  }
}

///|
/// Compatibility wrapper for `PdfDocument::transform_annotations`.
pub fn pdf_transform_annotations(
  document : PdfDocument,
  matrix : @geometry.TransformMatrix,
  rest : @syntax.PdfObject,
) -> Unit raise @core.PdfError {
  document.transform_annotations(matrix, rest)
}