///|
fn pdf_texttopdf_font_id(font_index : Int) -> String {
  font_index.to_string()
}

///|
fn pdf_texttopdf_name(text : String) -> PdfName {
  pdf_name_of_bytes(@ascii.encode(text))
}

///|
let pdf_texttopdf_struct_parents_key_name : PdfName = pdf_texttopdf_name(
  "/StructParents",
)

///|
fn pdf_texttopdf_struct_parents_key() -> PdfName {
  pdf_texttopdf_struct_parents_key_name
}

///|
let pdf_texttopdf_type_key_name : PdfName = pdf_texttopdf_name("/Type")

///|
fn pdf_texttopdf_type_key() -> PdfName {
  pdf_texttopdf_type_key_name
}

///|
let pdf_texttopdf_struct_tree_root_name : PdfName = pdf_texttopdf_name(
  "/StructTreeRoot",
)

///|
fn pdf_texttopdf_struct_tree_root() -> PdfName {
  pdf_texttopdf_struct_tree_root_name
}

///|
let pdf_texttopdf_mcr_name : PdfName = pdf_texttopdf_name("/MCR")

///|
fn pdf_texttopdf_mcr() -> PdfName {
  pdf_texttopdf_mcr_name
}

///|
let pdf_texttopdf_pg_key_name : PdfName = pdf_texttopdf_name("/Pg")

///|
fn pdf_texttopdf_pg_key() -> PdfName {
  pdf_texttopdf_pg_key_name
}

///|
let pdf_texttopdf_mcid_key_name : PdfName = pdf_texttopdf_name("/MCID")

///|
fn pdf_texttopdf_mcid_key() -> PdfName {
  pdf_texttopdf_mcid_key_name
}

///|
let pdf_texttopdf_s_key_name : PdfName = pdf_texttopdf_name("/S")

///|
fn pdf_texttopdf_s_key() -> PdfName {
  pdf_texttopdf_s_key_name
}

///|
let pdf_texttopdf_p_name : PdfName = pdf_texttopdf_name("/P")

///|
fn pdf_texttopdf_p() -> PdfName {
  pdf_texttopdf_p_name
}

///|
fn pdf_texttopdf_p_key() -> PdfName {
  pdf_texttopdf_p_name
}

///|
let pdf_texttopdf_document_name_value : PdfName = pdf_texttopdf_name(
  "/Document",
)

///|
fn pdf_texttopdf_document_name() -> PdfName {
  pdf_texttopdf_document_name_value
}

///|
let pdf_texttopdf_ns_key_name : PdfName = pdf_texttopdf_name("/NS")

///|
fn pdf_texttopdf_ns_key() -> PdfName {
  pdf_texttopdf_ns_key_name
}

///|
let pdf_texttopdf_namespaces_key_name : PdfName = pdf_texttopdf_name(
  "/Namespaces",
)

///|
fn pdf_texttopdf_namespaces_key() -> PdfName {
  pdf_texttopdf_namespaces_key_name
}

///|
let pdf_texttopdf_pdfua2_namespace : PdfBytes = @ascii.encode(
  "http://iso.org/pdf2/ssn",
)

///|
fn pdf_texttopdf_push_text(
  output : Array[PdfTypeElement],
  text : Array[Byte],
) -> Unit {
  output.push(PdfTypeText(Bytes::from_array(text)))
}

///|
fn pdf_texttopdf_process_codepoints(
  fontpack : PdfFontPack,
  font_size : Double,
  current_font : Ref[Int],
  output : Array[PdfTypeElement],
  codepoints : ArrayView[Int],
) -> Unit raise PdfError {
  let mut current_text : Array[Byte] = []
  for codepoint in codepoints {
    match fontpack.get_char(codepoint) {
      Some((charcode, font_index, font)) => {
        guard charcode >= 0 && charcode < 256 else { raise BadText }
        if font_index != current_font.val {
          if current_text.length() > 0 {
            pdf_texttopdf_push_text(output, current_text)
            current_text = []
          }
          current_font.val = font_index
          output.push(
            PdfTypeFont(pdf_texttopdf_font_id(font_index), font, font_size),
          )
        }
        current_text.push(charcode.to_byte())
      }
      None => ()
    }
  }
  pdf_texttopdf_push_text(output, current_text)
}

///|
/// Convert UTF-8 text into cpdftype instructions using a font pack.
///
/// Newlines become `PdfTypeNewLine`, carriage returns are ignored, and
/// representable Unicode codepoints are emitted as one-byte PDF charcodes.
pub fn pdf_texttopdf_of_utf8_with_newlines(
  fontpack : PdfFontPack,
  font_size : Double,
  text : BytesView,
) -> Array[PdfTypeElement] raise PdfError {
  let output : Array[PdfTypeElement] = []
  let current_font : Ref[Int] = { val: -1 }
  let codepoints = pdf_codepoints_of_utf8(text)
  let mut buffer : Array[Int] = []
  for codepoint in codepoints {
    match codepoint {
      10 => {
        if buffer.length() > 0 {
          pdf_texttopdf_process_codepoints(
            fontpack, font_size, current_font, output, buffer,
          )
        }
        output.push(PdfTypeNewLine)
        buffer = []
      }
      13 => ()
      _ => buffer.push(codepoint)
    }
  }
  if buffer.length() > 0 {
    pdf_texttopdf_process_codepoints(
      fontpack, font_size, current_font, output, buffer,
    )
  }
  output
}

///|
/// Add paragraph tags around a cpdftype stream.
///
/// This mirrors `cpdftexttopdf.ml`: a blank line closes the current paragraph,
/// preserves both line breaks, and starts a new paragraph with tag number zero.
pub fn pdf_texttopdf_tag_paragraphs(
  elements : ArrayView[PdfTypeElement],
) -> Array[PdfTypeElement] {
  let output : Array[PdfTypeElement] = [PdfTypeTag("P", 0)]
  let mut index = 0
  while index < elements.length() {
    match elements[index] {
      PdfTypeNewLine if index + 1 < elements.length() &&
        elements[index + 1] is PdfTypeNewLine => {
        output.push(PdfTypeEndTag)
        output.push(PdfTypeNewLine)
        output.push(PdfTypeNewLine)
        output.push(PdfTypeTag("P", 0))
        index += 2
      }
      element => {
        output.push(element)
        index += 1
      }
    }
  }
  output.push(PdfTypeEndTag)
  output
}

///|
fn pdf_texttopdf_first_font(
  elements : ArrayView[PdfTypeElement],
) -> PdfTypeElement? {
  let mut found = None
  for element in elements {
    match element {
      PdfTypeFont(_, _, _) if found is None => found = Some(element)
      _ => ()
    }
  }
  found
}

///|
fn pdf_texttopdf_document_elements(
  elements : ArrayView[PdfTypeElement],
) -> Array[PdfTypeElement] {
  match pdf_texttopdf_first_font(elements) {
    Some(first_font) => {
      let output : Array[PdfTypeElement] = [first_font, PdfTypeBeginDocument]
      for element in elements {
        output.push(element)
      }
      output
    }
    None => []
  }
}

///|
fn pdf_texttopdf_struct_parent_pages(
  pages : ArrayView[PdfPage],
) -> Array[PdfPage] raise PdfError {
  let output : Array[PdfPage] = []
  for index, page in pages {
    output.push({
      ..page,
      rest: page.rest.add_dict_entry(
        pdf_texttopdf_struct_parents_key(),
        PdfInteger(index),
      ),
    })
  }
  output
}

///|
fn pdf_texttopdf_tag_triples(
  tags : ArrayView[Array[(String, Int)]],
) -> Array[(Int, Int, Int)] {
  let output : Array[(Int, Int, Int)] = []
  for page_index in 0.. Array[Array[(Int, Int, Int)]] {
  let output : Array[Array[(Int, Int, Int)]] = []
  for triple in triples {
    if output.length() == 0 {
      output.push([triple])
    } else {
      let current = output[output.length() - 1]
      if current[0].0 == triple.0 {
        current.push(triple)
      } else {
        output.push([triple])
      }
    }
  }
  output
}

///|
fn pdf_texttopdf_page_reference(
  page_references : ArrayView[Int],
  page_index : Int,
) -> PdfObject raise PdfError {
  guard page_index >= 0 && page_index < page_references.length() else {
    raise BadPageSpecification("texttopdf: page reference not found")
  }
  PdfIndirect(page_references[page_index])
}

///|
fn pdf_texttopdf_structure_elements(
  document : PdfDocument,
  parent_number : Int,
  page_references : ArrayView[Int],
  nodes : ArrayView[Array[(Int, Int, Int)]],
) -> Array[PdfObject] raise PdfError {
  let output : Array[PdfObject] = []
  for parts in nodes {
    let kids : Array[PdfObject] = []
    for part in parts {
      let (_, page_index, mcid) = part
      kids.push(
        PdfDictionary([
          (pdf_texttopdf_type_key(), PdfNameObject(pdf_texttopdf_mcr())),
          (
            pdf_texttopdf_pg_key(),
            pdf_texttopdf_page_reference(page_references, page_index),
          ),
          (pdf_texttopdf_mcid_key(), PdfInteger(mcid)),
        ]),
      )
    }
    output.push(
      PdfIndirect(
        document.add_object(
          PdfDictionary([
            (pdf_structure_k_key(), PdfArray(kids)),
            (pdf_texttopdf_p_key(), PdfIndirect(parent_number)),
            (pdf_texttopdf_s_key(), PdfNameObject(pdf_texttopdf_p())),
          ]),
        ),
      ),
    )
  }
  output
}

///|
fn pdf_texttopdf_structure_element_for_tag(
  structure_elements : ArrayView[PdfObject],
  tag_number : Int,
) -> PdfObject raise PdfError {
  guard tag_number >= 0 && tag_number < structure_elements.length() else {
    raise BadPageSpecification("texttopdf: structure tag not found")
  }
  structure_elements[tag_number]
}

///|
fn pdf_texttopdf_parent_tree_entries(
  page_count : Int,
  triples : ArrayView[(Int, Int, Int)],
  structure_elements : ArrayView[PdfObject],
) -> Array[(Int, PdfObject)] raise PdfError {
  let output : Array[(Int, PdfObject)] = []
  for page_index in 0.. Unit raise PdfError {
  let triples = pdf_texttopdf_tag_triples(tags)
  let nodes = pdf_texttopdf_group_tag_triples(triples)
  let structure_root_number = self.add_object(PdfNull)
  let namespace_object = match subformat {
    Some(PdfUA2) =>
      Some(
        PdfIndirect(
          self.add_object(
            PdfDictionary([
              (
                pdf_texttopdf_ns_key(),
                PdfString(pdf_texttopdf_pdfua2_namespace),
              ),
            ]),
          ),
        ),
      )
    _ => None
  }
  let document_number = match subformat {
    Some(PdfUA2) => self.add_object(PdfNull)
    _ => 0
  }
  let parent_number = match subformat {
    Some(PdfUA2) => document_number
    _ => structure_root_number
  }
  let page_references = self.page_reference_numbers()
  let structure_elements = pdf_texttopdf_structure_elements(
    self, parent_number, page_references, nodes,
  )
  let parent_tree = self.add_object(
    self.build_number_tree(
      pdf_texttopdf_parent_tree_entries(
        tags.length(),
        triples,
        structure_elements,
      ),
    ),
  )
  let root_entries : Array[(PdfName, PdfObject)] = [
    (pdf_texttopdf_type_key(), PdfNameObject(pdf_texttopdf_struct_tree_root())),
    (pdf_structure_parent_tree_key(), PdfIndirect(parent_tree)),
  ]
  let root_k = match (subformat, namespace_object) {
    (Some(PdfUA2), Some(ns_object)) => {
      self.add_object_given_number(
        document_number,
        PdfDictionary([
          (pdf_structure_k_key(), PdfArray(structure_elements)),
          (pdf_texttopdf_p_key(), PdfIndirect(structure_root_number)),
          (pdf_texttopdf_s_key(), PdfNameObject(pdf_texttopdf_document_name())),
          (pdf_texttopdf_ns_key(), ns_object),
        ]),
      )
      root_entries.push((pdf_texttopdf_namespaces_key(), PdfArray([ns_object])))
      PdfIndirect(document_number)
    }
    _ => PdfArray(structure_elements)
  }
  root_entries.push((pdf_structure_k_key(), root_k))
  self.add_object_given_number(
    structure_root_number,
    PdfDictionary(root_entries),
  )
  self.replace_chain(
    [pdf_structure_root_key(), pdf_structure_struct_tree_root_key()],
    PdfIndirect(structure_root_number),
  )
}

///|
/// Typeset UTF-8 text into a standalone PDF document using an existing font
/// pack.
///
/// When `process_struct_tree` is set, paragraph tags are added and a
/// `/StructTreeRoot` with page-level parent-tree entries is installed.
pub fn pdf_texttopdf_typeset(
  fontpack : PdfFontPack,
  font_size : Double,
  paper : PaperSize,
  text : BytesView,
  process_struct_tree? : Bool = false,
  subformat? : PdfUASubformat? = None,
  title? : String? = None,
) -> PdfDocument raise PdfError {
  let process_struct_tree = process_struct_tree || subformat != None
  let title = match (subformat, title) {
    (Some(_), Some(title)) => Some(title)
    (Some(_), None) => raise TitleExpected
    (None, title) => title
  }
  let document = pdf_document_empty()
  let instructions = pdf_texttopdf_of_utf8_with_newlines(
    fontpack, font_size, text,
  )
  let tagged = if process_struct_tree {
    pdf_texttopdf_tag_paragraphs(instructions)
  } else {
    instructions
  }
  let elements = pdf_texttopdf_document_elements(tagged)
  let margin = unit_points(paper.width(), paper.unit()) / 15.0
  let result = document.pdf_type_typeset(
    process_struct_tree, margin, margin, margin, margin, paper, elements,
  )
  let pages = if process_struct_tree {
    pdf_texttopdf_struct_parent_pages(result.pages)
  } else {
    result.pages
  }
  let page_root = document.add_pagetree(pages)
  ignore(document.add_root(page_root, []))
  if process_struct_tree {
    document.pdf_texttopdf_add_structure_tree(result.tags, subformat)
  }
  match (subformat, title) {
    (Some(subformat), Some(title)) =>
      document.pdf_ua_apply_subformat_metadata(subformat, title)
    _ => ()
  }
  document
}