///|
/// One font resource entry reported by cpdf-style font listing.
///
/// `page` is one-based. `name`, `subtype`, `basefont`, and `encoding` are PDF
/// names rendered with their leading slash; missing non-name fields are empty
/// strings to match cpdf's text listing and JSON-null conversion.
pub(all) struct PdfFontListEntry {
  page : Int
  name : String
  subtype : String
  basefont : String
  encoding : String
  font : PdfObject
} derive(Debug, Eq, ToJson)

///|
/// One cpdf-style missing-font report entry.
///
/// `encoding == "Built-in"` records cpdf's fallback when a missing font does
/// not have a named `/Encoding`.
pub(all) struct PdfMissingFontEntry {
  page : Int
  name : String
  subtype : String
  basefont : String
  encoding : String
} derive(Debug, Eq, ToJson)

///|
/// One row from cpdf's font encoding table.
///
/// `unicode_name` is empty when this port has no Unicode character-name table
/// entry for the codepoint. `utf8` is `` for C0/C1 controls.
pub(all) struct PdfFontTableEntry {
  code : Int
  unicode : String
  utf8 : String
  unicode_name : String
  glyphnames : String
} derive(Debug, Eq, ToJson)

///|
fn PdfDocument::pdf_font_name_field(
  self : PdfDocument,
  font : PdfObject,
  key : PdfName,
) -> String raise PdfError {
  match self.lookup_direct(key, font) {
    Some(PdfNameObject(name)) => pdf_util_string_from_name(name)
    _ => ""
  }
}

///|
fn PdfDocument::pdf_font_list_entry(
  self : PdfDocument,
  page : Int,
  name : String,
  font : PdfObject,
) -> PdfFontListEntry raise PdfError {
  let direct_font = self.direct(font)
  {
    page,
    name,
    subtype: self.pdf_font_name_field(direct_font, pdf_text_subtype_key_name),
    basefont: self.pdf_font_name_field(direct_font, pdf_text_base_font_key_name),
    encoding: self.pdf_font_name_field(direct_font, pdf_text_encoding_key_name),
    font: direct_font,
  }
}

///|
fn PdfDocument::pdf_font_list_fonts_dictionary(
  self : PdfDocument,
  page : Int,
  prefix : String,
  fonts : ArrayView[(PdfName, PdfObject)],
) -> Array[PdfFontListEntry] raise PdfError {
  let output : Array[PdfFontListEntry] = []
  for entry in fonts {
    output.push(
      self.pdf_font_list_entry(
        page,
        prefix + pdf_util_string_from_name(entry.0),
        entry.1,
      ),
    )
  }
  output
}

///|
fn PdfDocument::pdf_font_list_xobjects(
  self : PdfDocument,
  page : Int,
  prefix : String,
  xobjects : ArrayView[(PdfName, PdfObject)],
  seen : Array[Int],
) -> Array[PdfFontListEntry] raise PdfError {
  let output : Array[PdfFontListEntry] = []
  for entry in xobjects {
    match entry.1 {
      PdfIndirect(number) =>
        output.append(
          self.pdf_font_list_xobject(
            page,
            prefix + pdf_util_string_from_name(entry.0),
            number,
            seen,
          ),
        )
      _ => ()
    }
  }
  output
}

///|
fn PdfDocument::pdf_font_list_resources(
  self : PdfDocument,
  page : Int,
  prefix : String,
  resources : PdfObject,
  seen : Array[Int],
) -> Array[PdfFontListEntry] raise PdfError {
  let output : Array[PdfFontListEntry] = []
  match self.lookup_direct(pdf_page_font_resource_name, resources) {
    Some(PdfDictionary(fonts)) =>
      output.append(self.pdf_font_list_fonts_dictionary(page, prefix, fonts))
    _ => ()
  }
  match self.lookup_direct(pdf_page_xobject_key(), resources) {
    Some(PdfDictionary(xobjects)) =>
      output.append(self.pdf_font_list_xobjects(page, prefix, xobjects, seen))
    _ => ()
  }
  output
}

///|
fn PdfDocument::pdf_font_list_xobject(
  self : PdfDocument,
  page : Int,
  prefix : String,
  xobject_number : Int,
  seen : Array[Int],
) -> Array[PdfFontListEntry] raise PdfError {
  if seen.contains(xobject_number) {
    []
  } else {
    seen.push(xobject_number)
    let xobject = self.lookup_object_or_null(xobject_number)
    match self.lookup_direct(pdf_page_resources_key(), xobject) {
      Some(resources) =>
        self.pdf_font_list_resources(page, prefix, resources, seen)
      _ => []
    }
  }
}

///|
/// Return cpdf's font resource listing for selected pages.
///
/// The listing walks page `/Resources` dictionaries and nested Form XObject
/// resources. Duplicate indirect XObjects are processed once per call, matching
/// cpdf's intended "do not process an xobject twice" behavior.
pub fn PdfDocument::list_fonts(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Array[PdfFontListEntry] raise PdfError {
  let pages = self.pages_of_pagetree()
  pdf_page_box_validate_page_numbers(page_numbers, pages.length(), "list_fonts")
  let output : Array[PdfFontListEntry] = []
  let seen_xobjects : Array[Int] = []
  for index, page in pages {
    let page_number = index + 1
    if pdf_page_box_contains_page(page_numbers, page_number) {
      output.append(
        self.pdf_font_list_resources(
          page_number,
          "",
          page.resources,
          seen_xobjects,
        ),
      )
    }
  }
  output
}

///|
/// Compatibility wrapper for `PdfDocument::list_fonts`.
pub fn pdf_list_fonts(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Array[PdfFontListEntry] raise PdfError {
  document.list_fonts(page_numbers)
}

///|
fn pdf_font_write_utf8(
  output : Array[Byte],
  position : Int,
  text : String,
) -> Int {
  let mut current = position
  for byte in @utf8.encode(text) {
    output[current] = byte
    current += 1
  }
  current
}

///|
fn pdf_font_write_listing_line(
  output : Array[Byte],
  position : Int,
  entry : PdfFontListEntry,
) -> Int {
  let mut current = pdf_font_write_utf8(
    output,
    position,
    entry.page.to_string(),
  )
  output[current] = 32
  current += 1
  current = pdf_font_write_utf8(output, current, entry.name)
  output[current] = 32
  current += 1
  current = pdf_font_write_utf8(output, current, entry.subtype)
  output[current] = 32
  current += 1
  current = pdf_font_write_utf8(output, current, entry.basefont)
  output[current] = 32
  current += 1
  current = pdf_font_write_utf8(output, current, entry.encoding)
  output[current] = 10
  current + 1
}

///|
fn pdf_font_utf8_length(text : String) -> Int {
  @utf8.encode(text).length()
}

///|
fn pdf_font_listing_line_length(entry : PdfFontListEntry) -> Int {
  pdf_font_utf8_length(entry.page.to_string()) +
  pdf_font_utf8_length(entry.name) +
  pdf_font_utf8_length(entry.subtype) +
  pdf_font_utf8_length(entry.basefont) +
  pdf_font_utf8_length(entry.encoding) +
  5
}

///|
fn pdf_font_listing_text_length(entries : ArrayView[PdfFontListEntry]) -> Int {
  let mut length = 0
  for entry in entries {
    length += pdf_font_listing_line_length(entry)
  }
  length
}

///|
/// Render cpdf's plain-text font listing for selected pages.
pub fn PdfDocument::list_fonts_text(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> PdfBytes raise PdfError {
  let entries = self.list_fonts(page_numbers)
  let output = Array::make(pdf_font_listing_text_length(entries), b'\x00')
  let mut position = 0
  for entry in entries {
    position = pdf_font_write_listing_line(output, position, entry)
  }
  Bytes::from_array(output)
}

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

///|
/// Return the bytes cpdf's `print_fonts` would print.
pub fn PdfDocument::print_fonts(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
  json? : Bool = false,
) -> PdfBytes raise PdfError {
  if json {
    self.fonts_json_blob(page_numbers)
  } else {
    self.list_fonts_text(page_numbers)
  }
}

///|
/// Compatibility wrapper matching cpdffont's `print_fonts`.
pub fn pdf_print_fonts(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
  json? : Bool = false,
) -> PdfBytes raise PdfError {
  document.print_fonts(page_numbers, json~)
}

///|
fn pdf_font_json_nullable_name(value : String) -> Json {
  if value == "" {
    Json::null()
  } else {
    Json::string(value)
  }
}

///|
fn pdf_font_entry_json(entry : PdfFontListEntry) -> Json {
  Json::object(
    Map([
      ("page", pdf_util_json_number(entry.page)),
      ("name", Json::string(entry.name)),
      ("subtype", pdf_font_json_nullable_name(entry.subtype)),
      ("basefont", pdf_font_json_nullable_name(entry.basefont)),
      ("encoding", pdf_font_json_nullable_name(entry.encoding)),
    ]),
  )
}

///|
/// Return cpdf's JSON font listing for selected pages.
pub fn PdfDocument::fonts_json(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Json raise PdfError {
  Json::array(
    [
      for entry in self.list_fonts(page_numbers) => pdf_font_entry_json(entry)
    ],
  )
}

///|
/// Compatibility wrapper for `PdfDocument::fonts_json`.
pub fn pdf_fonts_json(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Json raise PdfError {
  document.fonts_json(page_numbers)
}

///|
/// Compatibility wrapper matching cpdffont's `json_fonts`.
pub fn pdf_json_fonts(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Json raise PdfError {
  document.fonts_json(page_numbers)
}

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

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

///|
fn pdf_font_name_segments(font_name : String) -> Array[String] {
  let segments : Array[String] = []
  let mut start = 0
  for index in 0.. start {
        segments.push(font_name[start:index].to_owned())
      }
      start = index + 1
    }
  }
  if start < font_name.length() {
    segments.push(font_name[start:].to_owned())
  }
  segments
}

///|
fn pdf_font_segment_name(segment : String) -> PdfName {
  pdf_name_of_bytes(@ascii.encode("/" + segment))
}

///|
/// Resolve a cpdf font name path on a page.
///
/// `font_name` is the name shown by `list_fonts`, such as `/F1` for a page font
/// or `/Fm/FX` for a font inside a Form XObject. Valid pages whose path is not
/// found return `PdfNull`.
pub fn PdfDocument::font_from_name(
  self : PdfDocument,
  font_name : String,
  page_number : Int,
) -> PdfObject raise PdfError {
  let pages = self.pages_of_pagetree()
  pdf_page_box_validate_page_numbers(
    [page_number],
    pages.length(),
    "font_from_name",
  )
  let segments = pdf_font_name_segments(font_name)
  if segments.length() == 0 {
    PdfNull
  } else {
    let mut resources = pages[page_number - 1].resources
    let mut found = PdfNull
    let mut failed = false
    for index, segment in segments {
      if !failed {
        let key = pdf_font_segment_name(segment)
        if index == segments.length() - 1 {
          match self.lookup_direct(pdf_page_font_resource_name, resources) {
            Some(fonts) =>
              match self.lookup_direct(key, fonts) {
                Some(font) => found = font
                None => failed = true
              }
            None => failed = true
          }
        } else {
          match self.lookup_direct(pdf_page_xobject_key(), resources) {
            Some(xobjects) =>
              match self.lookup_direct(key, xobjects) {
                Some(xobject) =>
                  match self.lookup_direct(pdf_page_resources_key(), xobject) {
                    Some(next_resources) => resources = next_resources
                    None => failed = true
                  }
                None => failed = true
              }
            None => failed = true
          }
        }
      }
    }
    if failed {
      PdfNull
    } else {
      found
    }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::font_from_name`.
pub fn pdf_font_from_name(
  document : PdfDocument,
  font_name : String,
  page_number : Int,
) -> PdfObject raise PdfError {
  document.font_from_name(font_name, page_number)
}

///|
fn PdfDocument::pdf_font_table_target_font(
  self : PdfDocument,
  font_name : String,
  page_number : Int,
) -> PdfObject raise PdfError {
  let font = self.font_from_name(font_name, page_number)
  let mut selected = font
  match font {
    PdfDictionary(entries) =>
      for entry in entries {
        match self.lookup_direct(pdf_text_base_font_key_name, entry.1) {
          Some(PdfNameObject(name)) if pdf_util_string_from_name(name) ==
            font_name => selected = entry.1
          _ => ()
        }
      }
    _ => ()
  }
  selected
}

///|
fn pdf_font_table_charset(font : PdfFont) -> Array[PdfName]? {
  match font {
    PdfFontSimple({ fontdescriptor: Some({ charset: Some(charset), .. }), .. }) =>
      Some(charset)
    _ => None
  }
}

///|
fn pdf_font_table_text_bytes(
  font : PdfFont,
  code : Int,
) -> PdfBytes raise PdfError {
  if font.uses_two_byte_codes() {
    Bytes::from_array([pdf_byte_of_int(0), pdf_byte_of_int(code)])
  } else {
    Bytes::from_array([pdf_byte_of_int(code)])
  }
}

///|
fn pdf_font_table_strip_name_slash(name : PdfName) -> String {
  let text = pdf_util_string_from_name(name) catch {
    _ => @ascii.decode_lossy(name.bytes())
  }
  if text.has_prefix("/") {
    text[1:].to_owned()
  } else {
    text
  }
}

///|
fn pdf_font_table_glyphnames_text(glyphnames : ArrayView[PdfName]) -> String {
  let mut text = ""
  for glyphname in glyphnames {
    text = text + pdf_font_table_strip_name_slash(glyphname)
  }
  text
}

///|
fn pdf_font_table_in_charset(
  glyphnames : String,
  charset : ArrayView[PdfName],
) -> Bool {
  for name in charset {
    if pdf_font_table_strip_name_slash(name) == glyphnames {
      return true
    }
  }
  false
}

///|
fn pdf_font_table_utf8_text(
  codepoints : ArrayView[Int],
) -> String raise PdfError {
  let bytes = pdf_utf8_of_codepoints(codepoints)
  @utf8.decode(bytes) catch {
    _ => ""
  }
}

///|
fn pdf_font_table_unicode_fields(
  codepoints : ArrayView[Int],
) -> (String, String, String) raise PdfError {
  match codepoints {
    [codepoint] => {
      let data = pdf_unicode_data_lookup(codepoint)
      let unicode = match data {
        Some(data) => data.code_value
        None => ""
      }
      let unicode_name = match data {
        Some(data) => data.character_name
        None => ""
      }
      let is_control = match data {
        Some(data) => data.general_category == "Cc"
        None => false
      }
      let utf8 = if is_control {
        ""
      } else {
        pdf_font_table_utf8_text(codepoints)
      }
      (unicode, utf8, unicode_name)
    }
    _ => ("***multiple", pdf_font_table_utf8_text(codepoints), "***multiple")
  }
}

///|
fn pdf_font_table_entry(
  font : PdfFont,
  extractor : PdfTextExtractor,
  code : Int,
) -> PdfFontTableEntry? raise PdfError {
  let text = pdf_font_table_text_bytes(font, code)
  let glyphnames = extractor.glyphnames_of_text(text)
  let glyphnames_text = pdf_font_table_glyphnames_text(glyphnames)
  if glyphnames_text == ".notdef" {
    None
  } else {
    match pdf_font_table_charset(font) {
      Some(charset) if !pdf_font_table_in_charset(glyphnames_text, charset) =>
        None
      _ => {
        let codepoints = extractor.codepoints_of_text(text)
        let (unicode, utf8, unicode_name) = pdf_font_table_unicode_fields(
          codepoints,
        )
        Some({ code, unicode, utf8, unicode_name, glyphnames: glyphnames_text })
      }
    }
  }
}

///|
/// Return cpdf's font encoding table rows for one named page font.
///
/// This is the in-memory counterpart to cpdf's `print_font_table`. Font names
/// use the same page resource path syntax as `font_from_name`, such as `/F1`.
pub fn PdfDocument::font_table(
  self : PdfDocument,
  font_name : String,
  page_number : Int,
) -> Array[PdfFontTableEntry] raise PdfError {
  let font_object = self.pdf_font_table_target_font(font_name, page_number)
  let font = self.read_font(font_object)
  let extractor = font.text_extractor()
  let output : Array[PdfFontTableEntry] = []
  for code in 0..<256 {
    match pdf_font_table_entry(font, extractor, code) {
      Some(entry) => output.push(entry)
      None => ()
    }
  }
  output
}

///|
/// Compatibility wrapper for `PdfDocument::font_table`.
pub fn pdf_font_table(
  document : PdfDocument,
  font_name : String,
  page_number : Int,
) -> Array[PdfFontTableEntry] raise PdfError {
  document.font_table(font_name, page_number)
}

///|
fn pdf_font_write_table_line(
  output : Array[Byte],
  position : Int,
  entry : PdfFontTableEntry,
) -> Int {
  let mut current = pdf_font_write_utf8(
    output,
    position,
    entry.code.to_string(),
  )
  current = pdf_font_write_utf8(output, current, " = U+")
  current = pdf_font_write_utf8(output, current, entry.unicode)
  current = pdf_font_write_utf8(output, current, " (")
  current = pdf_font_write_utf8(output, current, entry.utf8)
  current = pdf_font_write_utf8(output, current, " - ")
  current = pdf_font_write_utf8(output, current, entry.unicode_name)
  current = pdf_font_write_utf8(output, current, ") = ")
  current = pdf_font_write_utf8(output, current, entry.glyphnames)
  output[current] = 10
  current + 1
}

///|
fn pdf_font_table_line_length(entry : PdfFontTableEntry) -> Int {
  pdf_font_utf8_length(entry.code.to_string()) +
  pdf_font_utf8_length(entry.unicode) +
  pdf_font_utf8_length(entry.utf8) +
  pdf_font_utf8_length(entry.unicode_name) +
  pdf_font_utf8_length(entry.glyphnames) +
  15
}

///|
fn pdf_font_table_text_length(entries : ArrayView[PdfFontTableEntry]) -> Int {
  let mut length = 0
  for entry in entries {
    length += pdf_font_table_line_length(entry)
  }
  length
}

///|
/// Render cpdf's plain-text font encoding table for one named page font.
pub fn PdfDocument::font_table_text(
  self : PdfDocument,
  font_name : String,
  page_number : Int,
) -> PdfBytes raise PdfError {
  let entries = self.font_table(font_name, page_number)
  let output = Array::make(pdf_font_table_text_length(entries), b'\x00')
  let mut position = 0
  for entry in entries {
    position = pdf_font_write_table_line(output, position, entry)
  }
  Bytes::from_array(output)
}

///|
/// Compatibility wrapper for `PdfDocument::font_table_text`.
pub fn pdf_font_table_text(
  document : PdfDocument,
  font_name : String,
  page_number : Int,
) -> PdfBytes raise PdfError {
  document.font_table_text(font_name, page_number)
}

///|
/// Return the bytes cpdf's `print_font_table` would print.
pub fn PdfDocument::print_font_table(
  self : PdfDocument,
  font_name : String,
  page_number : Int,
) -> PdfBytes raise PdfError {
  self.font_table_text(font_name, page_number)
}

///|
/// Compatibility wrapper matching cpdffont's `print_font_table`.
pub fn pdf_print_font_table(
  document : PdfDocument,
  font_name : String,
  page_number : Int,
) -> PdfBytes raise PdfError {
  document.print_font_table(font_name, page_number)
}

///|
/// Return the object number referenced by an embedded font-file marker.
pub fn PdfFontFile::object_number(self : PdfFontFile) -> Int {
  match self {
    PdfFontFile(number) | PdfFontFile2(number) | PdfFontFile3(number) => number
  }
}

///|
/// Return the embedded font-file stream object number for a parsed font.
pub fn PdfFont::embedded_fontfile_number(self : PdfFont) -> Int? {
  match self {
    PdfFontSimple(
      { fontdescriptor: Some({ fontfile: Some(fontfile), .. }), .. }
    ) => Some(fontfile.object_number())
    PdfFontCIDKeyed(
      {
        composite: { cid_fontdescriptor: { fontfile: Some(fontfile), .. }, .. },
        ..,
      }
    ) => Some(fontfile.object_number())
    _ => None
  }
}

///|
fn PdfDocument::pdf_font_descriptor_file_number(
  self : PdfDocument,
  font : PdfObject,
) -> Int? {
  match self.lookup_direct(pdf_text_font_descriptor_key_name, font) {
    Some(descriptor) =>
      match pdf_text_fontfile_of_descriptor(descriptor) {
        Some(fontfile) => Some(fontfile.object_number())
        None => None
      }
    None => None
  }
}

///|
fn PdfDocument::pdf_font_embedded_file_number(
  self : PdfDocument,
  font : PdfObject,
) -> Int? {
  match self.pdf_font_descriptor_file_number(font) {
    Some(number) => Some(number)
    None =>
      match self.lookup_direct(pdf_text_descendant_fonts_key_name, font) {
        Some(PdfArray(descendants)) =>
          for descendant in descendants {
            match self.pdf_font_descriptor_file_number(descendant) {
              Some(number) => break Some(number)
              None => continue
            }
          } nobreak {
            None
          }
        _ => None
      }
  }
}

///|
/// Return decoded embedded font-file bytes for a named page font, if present.
///
/// This is the in-memory counterpart to cpdf's `extract_fontfile`, which writes
/// the decoded font program to a path. Missing names or fonts without embedded
/// font programs return `None`; malformed font dictionaries or font-file stream
/// objects raise through the normal PDF error path.
pub fn PdfDocument::extract_fontfile_bytes(
  self : PdfDocument,
  page_number : Int,
  font_name : String,
) -> PdfBytes? raise PdfError {
  let font = self.font_from_name(font_name, page_number)
  if font == PdfNull {
    None
  } else {
    match self.pdf_font_embedded_file_number(font) {
      Some(number) =>
        Some(self.pdf_decode_stream(PdfIndirect(number)).stream_bytes())
      None => None
    }
  }
}

///|
/// Compatibility wrapper for `PdfDocument::extract_fontfile_bytes`.
pub fn pdf_extract_fontfile_bytes(
  document : PdfDocument,
  page_number : Int,
  font_name : String,
) -> PdfBytes? raise PdfError {
  document.extract_fontfile_bytes(page_number, font_name)
}

///|
fn PdfDocument::pdf_font_copy_referenced_objects_from(
  self : PdfDocument,
  source : PdfDocument,
  font : PdfObject,
) -> Unit {
  for number in source.referenced_object_numbers(font) {
    self.add_object_given_number(number, source.lookup_object_or_null(number))
  }
}

///|
fn PdfDocument::pdf_font_resource_dictionary_or_empty(
  self : PdfDocument,
  resources : PdfObject,
) -> PdfObject raise PdfError {
  match self.lookup_direct(pdf_page_font_resource_name, resources) {
    Some(PdfDictionary(_) as fonts) => fonts
    Some(_) => raise DictionaryExpected
    None => PdfDictionary([])
  }
}

///|
fn PdfDocument::pdf_font_add_to_page_resources(
  self : PdfDocument,
  page : PdfPage,
  resource_name : PdfName,
  font : PdfObject,
) -> PdfPage raise PdfError {
  let fonts = self.pdf_font_resource_dictionary_or_empty(page.resources)
  let updated_fonts = fonts.add_dict_entry(resource_name, font)
  {
    ..page,
    resources: page.resources.add_dict_entry(
      pdf_page_font_resource_name, updated_fonts,
    ),
  }
}

///|
/// Copy a named font from `source` onto selected pages of this document.
///
/// This ports cpdf's `copy_font`: both documents are first renumbered into one
/// non-overlapping object space, the direct source font dictionary is added to
/// selected target pages under its `/BaseFont` name, and all objects referenced
/// by the copied font dictionary are copied into the target document.
pub fn PdfDocument::copy_font_from(
  self : PdfDocument,
  source : PdfDocument,
  font_name : String,
  font_page : Int,
  page_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
  let renumbered = pdf_renumber_documents([source, self])
  let source_pdf = renumbered[0]
  let target_pdf = renumbered[1]
  let font = source_pdf.font_from_name(font_name, font_page)
  let resource_name = match
    source_pdf.lookup_direct(pdf_text_base_font_key_name, font) {
    Some(PdfNameObject(name)) => name
    _ => pdf_name_of_bytes(b"/CopyFontAddedNoName")
  }
  target_pdf.pdf_font_copy_referenced_objects_from(source_pdf, font)
  let pages = target_pdf.pages_of_pagetree()
  pdf_page_box_validate_page_numbers(page_numbers, pages.length(), "copy_font")
  let rewritten : Array[PdfPage] = []
  for index, page in pages {
    let page_number = index + 1
    if pdf_page_box_contains_page(page_numbers, page_number) {
      rewritten.push(
        target_pdf.pdf_font_add_to_page_resources(page, resource_name, font),
      )
    } else {
      rewritten.push(page)
    }
  }
  target_pdf.change_pages(rewritten)
}

///|
/// Compatibility wrapper for `PdfDocument::copy_font_from`.
pub fn pdf_copy_font(
  source : PdfDocument,
  font_name : String,
  font_page : Int,
  page_numbers : ArrayView[Int],
  target : PdfDocument,
) -> PdfDocument raise PdfError {
  target.copy_font_from(source, font_name, font_page, page_numbers)
}

///|
fn PdfDocument::pdf_font_has_embedded_file(
  self : PdfDocument,
  descriptor : PdfObject,
) -> Bool {
  self.lookup_direct(pdf_text_font_file_key_name, descriptor) is Some(_) ||
  self.lookup_direct(pdf_text_font_file2_key_name, descriptor) is Some(_) ||
  self.lookup_direct(pdf_text_font_file3_key_name, descriptor) is Some(_)
}

///|
/// Return whether a font dictionary has no embedded font program.
///
/// Type 3 fonts are never reported as missing, matching cpdf. Other fonts are
/// missing when they have no `/FontDescriptor`, or when their descriptor lacks
/// `/FontFile`, `/FontFile2`, and `/FontFile3`.
pub fn PdfDocument::is_missing_font(
  self : PdfDocument,
  font : PdfObject,
) -> Bool {
  match self.lookup_direct(pdf_text_subtype_key_name, font) {
    Some(PdfNameObject(name)) if name == pdf_text_type3_name => false
    _ =>
      match self.lookup_direct(pdf_text_font_descriptor_key_name, font) {
        None => true
        Some(descriptor) => !self.pdf_font_has_embedded_file(descriptor)
      }
  }
}

///|
fn PdfDocument::pdf_missing_font_entry(
  self : PdfDocument,
  page : Int,
  name : PdfName,
  font : PdfObject,
) -> PdfMissingFontEntry raise PdfError {
  let encoding = match
    self.pdf_font_name_field(font, pdf_text_encoding_key_name) {
    "" => "Built-in"
    value => value
  }
  {
    page,
    name: pdf_util_string_from_name(name),
    subtype: self.pdf_font_name_field(font, pdf_text_subtype_key_name),
    basefont: self.pdf_font_name_field(font, pdf_text_base_font_key_name),
    encoding,
  }
}

///|
fn PdfDocument::pdf_missing_font_candidates(
  self : PdfDocument,
  name : PdfName,
  font : PdfObject,
) -> Array[(PdfName, PdfObject)] {
  match self.lookup_direct(pdf_text_descendant_fonts_key_name, font) {
    Some(PdfArray(descendants)) =>
      [
        for descendant in descendants => (name, descendant)
      ]
    _ => [(name, font)]
  }
}

///|
fn PdfDocument::pdf_missing_fonts_on_page(
  self : PdfDocument,
  page : Int,
  resources : PdfObject,
) -> Array[PdfMissingFontEntry] raise PdfError {
  let output : Array[PdfMissingFontEntry] = []
  match self.lookup_direct(pdf_page_font_resource_name, resources) {
    Some(PdfDictionary(fonts)) =>
      for font_entry in fonts {
        for
          candidate in self.pdf_missing_font_candidates(
            font_entry.0,
            font_entry.1,
          ) {
          if self.is_missing_font(candidate.1) {
            output.push(
              self.pdf_missing_font_entry(page, candidate.0, candidate.1),
            )
          }
        }
      }
    _ => ()
  }
  output
}

///|
/// Return cpdf's missing-font report for selected pages.
///
/// This follows cpdf's current behavior by inspecting page `/Font` resources and
/// descendant fonts. It does not recurse into Form XObjects; the cpdf source
/// marks that as a separate FIXME.
pub fn PdfDocument::missing_fonts(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Array[PdfMissingFontEntry] raise PdfError {
  let pages = self.pages_of_pagetree()
  pdf_page_box_validate_page_numbers(
    page_numbers,
    pages.length(),
    "missing_fonts",
  )
  let output : Array[PdfMissingFontEntry] = []
  for index, page in pages {
    let page_number = index + 1
    if pdf_page_box_contains_page(page_numbers, page_number) {
      output.append(self.pdf_missing_fonts_on_page(page_number, page.resources))
    }
  }
  output
}

///|
/// Compatibility wrapper for `PdfDocument::missing_fonts`.
pub fn pdf_missing_fonts(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Array[PdfMissingFontEntry] raise PdfError {
  document.missing_fonts(page_numbers)
}

///|
fn pdf_missing_font_entry_tuple(
  entry : PdfMissingFontEntry,
) -> (Int, String, String, String, String) {
  (entry.page, entry.name, entry.subtype, entry.basefont, entry.encoding)
}

///|
/// Return cpdffont's `missing_fonts_return` tuple rows.
pub fn PdfDocument::missing_fonts_return(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Array[(Int, String, String, String, String)] raise PdfError {
  [
    for entry in self.missing_fonts(page_numbers) => {
      pdf_missing_font_entry_tuple(entry)
    }
  ]
}

///|
/// Compatibility wrapper for `PdfDocument::missing_fonts_return`.
pub fn pdf_missing_fonts_return(
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> Array[(Int, String, String, String, String)] raise PdfError {
  document.missing_fonts_return(page_numbers)
}

///|
fn pdf_missing_font_write_text(
  output : Array[Byte],
  position : Int,
  entry : PdfMissingFontEntry,
) -> Int {
  let mut current = pdf_font_write_utf8(
    output,
    position,
    entry.page.to_string(),
  )
  current = pdf_font_write_utf8(output, current, ", ")
  current = pdf_font_write_utf8(output, current, entry.name)
  current = pdf_font_write_utf8(output, current, ", ")
  current = pdf_font_write_utf8(output, current, entry.subtype)
  current = pdf_font_write_utf8(output, current, ", ")
  current = pdf_font_write_utf8(output, current, entry.basefont)
  current = pdf_font_write_utf8(output, current, ", ")
  current = pdf_font_write_utf8(output, current, entry.encoding)
  output[current] = 10
  current + 1
}

///|
fn pdf_missing_font_text_length(entry : PdfMissingFontEntry) -> Int {
  pdf_font_utf8_length(entry.page.to_string()) +
  pdf_font_utf8_length(entry.name) +
  pdf_font_utf8_length(entry.subtype) +
  pdf_font_utf8_length(entry.basefont) +
  pdf_font_utf8_length(entry.encoding) +
  9
}

///|
fn pdf_missing_fonts_text_length(
  entries : ArrayView[PdfMissingFontEntry],
) -> Int {
  let mut length = 0
  for entry in entries {
    length += pdf_missing_font_text_length(entry)
  }
  length
}

///|
/// Render cpdf's plain-text missing-font report for selected pages.
pub fn PdfDocument::missing_fonts_text(
  self : PdfDocument,
  page_numbers : ArrayView[Int],
) -> PdfBytes raise PdfError {
  let entries = self.missing_fonts(page_numbers)
  let output = Array::make(pdf_missing_fonts_text_length(entries), b'\x00')
  let mut position = 0
  for entry in entries {
    position = pdf_missing_font_write_text(output, position, entry)
  }
  Bytes::from_array(output)
}

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

///|
fn PdfDocument::pdf_font_remove_descriptor_files(
  self : PdfDocument,
  object : PdfObject,
) -> PdfObject raise PdfError {
  match object {
    PdfDictionary(_) =>
      match self.lookup_direct(pdf_text_type_key_name, object) {
        Some(PdfNameObject(kind)) if kind == pdf_text_font_name =>
          match self.lookup_direct(pdf_text_font_descriptor_key_name, object) {
            Some(PdfDictionary(_) | PdfStreamObject(_) as descriptor) => {
              let stripped = descriptor
                .remove_dict_entry(pdf_text_font_file_key_name)
                .remove_dict_entry(pdf_text_font_file2_key_name)
                .remove_dict_entry(pdf_text_font_file3_key_name)
              object.add_dict_entry(
                pdf_text_font_descriptor_key_name,
                PdfIndirect(self.add_object(stripped)),
              )
            }
            _ => object
          }
        _ => object
      }
    _ => object
  }
}

///|
/// Return a copy of the document with embedded font-file references removed.
///
/// This ports cpdf's `remove_fonts`: every parsed `/Type /Font` dictionary with
/// a `/FontDescriptor` receives a new descriptor object with `/FontFile`,
/// `/FontFile2`, and `/FontFile3` removed. The original document and original
/// descriptor objects are left untouched.
pub fn PdfDocument::remove_embedded_fonts(
  self : PdfDocument,
) -> PdfDocument raise PdfError {
  let pdf = self.copy()
  let entries = pdf.parsed_object_entries()
  for entry in entries {
    pdf.add_object_given_number(
      entry.0,
      pdf.pdf_font_remove_descriptor_files(entry.2),
      generation=entry.1,
    )
  }
  pdf
}

///|
/// Compatibility wrapper for `PdfDocument::remove_embedded_fonts`.
pub fn pdf_remove_fonts(document : PdfDocument) -> PdfDocument raise PdfError {
  document.remove_embedded_fonts()
}