///|
/// Convert UTF-8 text into the font/text runs used by cpdftoc.
///
/// Unrepresentable codepoints are skipped, matching cpdf's `option_map
/// (Cpdfembed.get_char fontpack)` behavior. Unlike the text-to-PDF paragraph
/// helper, this function treats input as a single run and does not interpret
/// newline characters.
pub fn pdf_toc_of_utf8(
  fontpack : PdfFontPack,
  font_size : Double,
  text : BytesView,
) -> Array[PdfTypeElement] raise @core.PdfError {
  let mapped : Array[(Int, Int, PdfFont)] = []
  for codepoint in pdf_codepoints_of_utf8(text) {
    match fontpack.get_char(codepoint) {
      Some(run) => mapped.push(run)
      None => ()
    }
  }
  let output : Array[PdfTypeElement] = []
  pdf_toc_push_font_text_runs(output, mapped, font_size)
  output
}

///|
fn pdf_toc_codepoints_contains(
  codepoints : ArrayView[Int],
  codepoint : Int,
) -> Bool {
  for current in codepoints {
    if current == codepoint {
      return true
    }
  }
  false
}

///|
fn pdf_toc_append_unique_codepoints(
  output : Array[Int],
  codepoints : ArrayView[Int],
) -> Unit {
  for codepoint in codepoints {
    if !pdf_toc_codepoints_contains(output, codepoint) {
      output.push(codepoint)
    }
  }
}

///|
fn pdf_toc_append_utf8_codepoints(
  output : Array[Int],
  text : BytesView,
) -> Unit raise @core.PdfError {
  pdf_toc_append_unique_codepoints(output, pdf_codepoints_of_utf8(text))
}

///|
fn pdf_toc_append_pdfdocstring_codepoints(
  output : Array[Int],
  text : BytesView,
) -> Unit raise @core.PdfError {
  pdf_toc_append_utf8_codepoints(output, pdf_utf8_of_pdfdocstring(text))
}

///|
fn pdf_toc_page_label_text(
  page_number : Int,
  labels : ArrayView[@label.PdfPageLabel],
) -> @core.PdfBytes {
  if page_number < 1 {
    pdf_toc_ascii(page_number.to_string())
  } else {
    match @label.pdf_page_label_text_of_page_number(page_number, labels) {
      Some(label) => label
      None => pdf_toc_ascii(page_number.to_string())
    }
  }
}

///|
/// Return the unique Unicode codepoints cpdftoc needs before embedding a font.
///
/// The scan includes the dot used by ellipses/leaders, the TOC title, bookmark
/// titles, and the visible page-label text for each bookmark target. The
/// returned order is first occurrence for deterministic callers and tests.
pub fn PdfDocument::toc_used_codepoints(
  self : PdfDocument,
  labels : ArrayView[@label.PdfPageLabel],
  title : BytesView,
  bookmarks : ArrayView[@bookmark.PdfBookmark],
) -> Array[Int] raise @core.PdfError {
  let used : Array[Int] = [46]
  pdf_toc_append_utf8_codepoints(used, title)
  for bookmark in bookmarks {
    pdf_toc_append_pdfdocstring_codepoints(used, bookmark.text)
    let page_number = self.page_number_of_target(bookmark.target)
    pdf_toc_append_pdfdocstring_codepoints(
      used,
      pdf_toc_page_label_text(page_number, labels),
    )
  }
  used
}

///|
/// Compatibility wrapper for `PdfDocument::toc_used_codepoints`.
pub fn pdf_toc_used_codepoints(
  document : PdfDocument,
  labels : ArrayView[@label.PdfPageLabel],
  title : BytesView,
  bookmarks : ArrayView[@bookmark.PdfBookmark],
) -> Array[Int] raise @core.PdfError {
  document.toc_used_codepoints(labels, title, bookmarks)
}

///|
/// Convert PDFDocEncoding/PDFDocString bytes into the font/text runs used by
/// cpdftoc.
pub fn pdf_toc_of_pdfdocencoding(
  fontpack : PdfFontPack,
  font_size : Double,
  text : BytesView,
) -> Array[PdfTypeElement] raise @core.PdfError {
  pdf_toc_of_utf8(fontpack, font_size, pdf_utf8_of_pdfdocstring(text))
}