///|
/// Build cpdf's URL link annotation object for add-text markup.
pub fn pdf_addtext_url_annotation(
  p0 : @geometry.Point2,
  p1 : @geometry.Point2,
  p2 : @geometry.Point2,
  p3 : @geometry.Point2,
  rectangle : @geometry.PdfRectangle,
  url : String,
  url_border : Bool,
) -> @syntax.PdfObject {
  PdfDictionary([
    (pdf_addtext_subtype_key_name, PdfNameObject(pdf_addtext_link_name)),
    (
      pdf_addtext_quadpoints_key_name,
      PdfArray([
        PdfReal(p0.x),
        PdfReal(p0.y),
        PdfReal(p3.x),
        PdfReal(p3.y),
        PdfReal(p1.x),
        PdfReal(p1.y),
        PdfReal(p2.x),
        PdfReal(p2.y),
      ]),
    ),
    (
      pdf_addtext_rect_key_name,
      PdfArray([
        PdfReal(rectangle.min_x),
        PdfReal(rectangle.min_y),
        PdfReal(rectangle.max_x),
        PdfReal(rectangle.max_y),
      ]),
    ),
    (
      pdf_addtext_bs_key_name,
      PdfDictionary([
        (pdf_addtext_w_key_name, PdfInteger(if url_border { 1 } else { 0 })),
      ]),
    ),
    (
      pdf_addtext_a_key_name,
      PdfDictionary([
        (pdf_addtext_uri_key_name, PdfString(@utf8.encode(url))),
        (pdf_addtext_type_key_name, PdfNameObject(pdf_addtext_action_name)),
        (pdf_addtext_s_key_name, PdfNameObject(pdf_addtext_uri_key_name)),
      ]),
    ),
  ])
}

///|
fn pdf_addtext_bounds_of_points(
  p0 : @geometry.Point2,
  p1 : @geometry.Point2,
  p2 : @geometry.Point2,
  p3 : @geometry.Point2,
) -> @geometry.PdfRectangle {
  {
    min_x: double_min(double_min(p0.x, p1.x), double_min(p2.x, p3.x)),
    min_y: double_min(double_min(p0.y, p1.y), double_min(p2.y, p3.y)),
    max_x: double_max(double_max(p0.x, p1.x), double_max(p2.x, p3.x)),
    max_y: double_max(double_max(p0.y, p1.y), double_max(p2.y, p3.y)),
  }
}

///|
/// Return cpdf's add-text URL annotation height for a font selection.
pub fn pdf_addtext_url_annotation_height(
  font : PdfFont?,
  fontname : String,
  font_size : Double,
) -> Double {
  match pdf_addtext_cap_height(font, fontname) {
    Some(cap_height) => cap_height * font_size / 1000.0
    None => font_size
  }
}

///|
/// Calculate the transformed quad points and bounds for an add-text URL span.
pub fn pdf_addtext_url_annotation_geometry(
  origin : @geometry.Point2,
  horizontal_offset : Double,
  vertical_offset : Double,
  justification_offset : Double,
  rotation : Double,
  rotation_offset : @geometry.Point2,
  page_rotation : Double,
  start_width : Double,
  end_width : Double,
  height : Double,
) -> @addtext.PdfAddTextUrlAnnotationGeometry {
  let base_x = origin.x - horizontal_offset - justification_offset
  let base_y = origin.y - vertical_offset
  let min_x = base_x + start_width
  let min_y = base_y
  let max_x = base_x + end_width
  let max_y = base_y + height
  let matrix = @geometry.transform_matrix_of_transform([
    Translate(rotation_offset.x, rotation_offset.y),
    Rotate(@geometry.point2(base_x, base_y), rotation + page_rotation),
  ])
  let p0 = matrix.apply(@geometry.point2(min_x, min_y))
  let p1 = matrix.apply(@geometry.point2(max_x, max_y))
  let p2 = matrix.apply(@geometry.point2(min_x, max_y))
  let p3 = matrix.apply(@geometry.point2(max_x, min_y))
  { p0, p1, p2, p3, rectangle: pdf_addtext_bounds_of_points(p0, p1, p2, p3), }
}

///|
fn pdf_addtext_url_annotation_prefix(
  text : String,
  position : Int,
) -> String raise @core.PdfError {
  let bytes = @utf8.encode(text)
  if position < 0 || position > bytes.length() {
    raise SoftError("bad add-text URL byte offset")
  }
  @utf8.decode(bytes[:position]) catch {
    _ => raise SoftError("bad add-text URL byte offset")
  }
}

///|
fn pdf_addtext_url_annotation_widths_with_measure(
  text : String,
  url : @addtext.PdfAddTextUrl,
  measure : (String) -> Double raise @core.PdfError,
) -> (Double, Double) raise @core.PdfError {
  (
    measure(pdf_addtext_url_annotation_prefix(text, url.start)),
    measure(pdf_addtext_url_annotation_prefix(text, url.end)),
  )
}

///|
/// Measure cpdf's start and end prefix widths for an add-text URL span.
pub fn pdf_addtext_url_annotation_widths(
  text : String,
  url : @addtext.PdfAddTextUrl,
  measure : (String) -> Double,
) -> (Double, Double) raise @core.PdfError {
  pdf_addtext_url_annotation_widths_with_measure(text, url, fn(prefix) {
    measure(prefix)
  })
}

///|
/// Build indirect URL link annotations for one cpdf add-text line.
///
/// This mirrors cpdf's add-text annotation pass: each parsed URL span is
/// measured against the visible line, converted to transformed quad points, and
/// written as a new indirect annotation object.
pub fn PdfDocument::addtext_url_annotation_references(
  self : PdfDocument,
  text : String,
  urls : ArrayView[@addtext.PdfAddTextUrl],
  measure : (String) -> Double raise @core.PdfError,
  origin : @geometry.Point2,
  horizontal_offset : Double,
  vertical_offset : Double,
  justification_offset : Double,
  rotation : Double,
  rotation_offset : @geometry.Point2,
  page_rotation : Double,
  font : PdfFont?,
  fontname : String,
  font_size : Double,
  url_border : Bool,
) -> Array[@syntax.PdfObject] raise @core.PdfError {
  let height = pdf_addtext_url_annotation_height(font, fontname, font_size)
  let annotations : Array[@syntax.PdfObject] = Array(capacity=urls.length())
  for url in urls {
    let (start_width, end_width) = pdf_addtext_url_annotation_widths_with_measure(
      text, url, measure,
    )
    let geometry = pdf_addtext_url_annotation_geometry(
      origin, horizontal_offset, vertical_offset, justification_offset, rotation,
      rotation_offset, page_rotation, start_width, end_width, height,
    )
    let object_number = self.add_object(
      pdf_addtext_url_annotation(
        geometry.p0,
        geometry.p1,
        geometry.p2,
        geometry.p3,
        geometry.rectangle,
        url.url,
        url_border,
      ),
    )
    annotations.push(PdfIndirect(object_number))
  }
  annotations
}

///|
/// Compatibility wrapper for `PdfDocument::addtext_url_annotation_references`.
pub fn pdf_addtext_url_annotation_references(
  document : PdfDocument,
  text : String,
  urls : ArrayView[@addtext.PdfAddTextUrl],
  measure : (String) -> Double raise @core.PdfError,
  origin : @geometry.Point2,
  horizontal_offset : Double,
  vertical_offset : Double,
  justification_offset : Double,
  rotation : Double,
  rotation_offset : @geometry.Point2,
  page_rotation : Double,
  font : PdfFont?,
  fontname : String,
  font_size : Double,
  url_border : Bool,
) -> Array[@syntax.PdfObject] raise @core.PdfError {
  document.addtext_url_annotation_references(
    text, urls, measure, origin, horizontal_offset, vertical_offset, justification_offset,
    rotation, rotation_offset, page_rotation, font, fontname, font_size, url_border,
  )
}

///|
/// Return a page rest dictionary with cpdf add-text annotations prepended.
///
/// Empty new annotation lists leave `rest` unchanged. Existing `/Annots`
/// arrays are resolved through `document`; missing or malformed values are
/// treated as empty, matching cpdf's add-text page update.
pub fn PdfDocument::addtext_page_rest_with_annotations(
  self : PdfDocument,
  rest : @syntax.PdfObject,
  annotations : ArrayView[@syntax.PdfObject],
) -> @syntax.PdfObject raise @core.PdfError {
  if annotations.length() == 0 {
    rest
  } else {
    let updated : Array[@syntax.PdfObject] = [
      for annotation in annotations => annotation
    ]
    match self.lookup_direct(pdf_addtext_annots_key(), rest) {
      Some(PdfArray(existing)) =>
        for annotation in existing {
          updated.push(annotation)
        }
      _ => ()
    }
    rest.add_dict_entry(pdf_addtext_annots_key(), PdfArray(updated))
  }
}

///|
/// Compatibility wrapper for `PdfDocument::addtext_page_rest_with_annotations`.
pub fn pdf_addtext_page_rest_with_annotations(
  document : PdfDocument,
  rest : @syntax.PdfObject,
  annotations : ArrayView[@syntax.PdfObject],
) -> @syntax.PdfObject raise @core.PdfError {
  document.addtext_page_rest_with_annotations(rest, annotations)
}