///|
fn pdf_truetype_required_subset_table(tag : String) -> Bool {
  tag == "head" ||
  tag == "hhea" ||
  tag == "loca" ||
  tag == "cmap" ||
  tag == "maxp" ||
  tag == "cvt " ||
  tag == "glyf" ||
  tag == "prep" ||
  tag == "hmtx" ||
  tag == "fpgm"
}

///|
/// Write the format-6 cmap table used by cpdf for implicit-font-file subsets.
///
/// Character codes start at `33`; each Unicode value in `subset_codepoints`
/// contributes its original glyph index, or `0` if the original cmap has no
/// mapping. The returned bytes are padded, while `length` is the unpadded table
/// length for directory metadata.
pub fn pdf_truetype_subset_cmap_table(
  data : BytesView,
  subset_codepoints : ArrayView[Int],
) -> @truetype.PdfTrueTypeSubsetTable raise @core.PdfError {
  let cmap_entries = pdf_truetype_cmap_glyphs(data)
  let glyph_indices : Array[Int] = []
  for codepoint in subset_codepoints {
    glyph_indices.push(
      match pdf_truetype_cmap_glyph_from_entries(cmap_entries, codepoint) {
        Some(glyph_index) => glyph_index
        None => 0
      },
    )
  }
  let length = 22 + glyph_indices.length() * 2
  let output = Array::make(length + pdf_truetype_table_padding(length), b'\x00')
  let mut position = pdf_truetype_write_u16_at(output, 0, 0)
  position = pdf_truetype_write_u16_at(output, position, 1)
  position = pdf_truetype_write_u16_at(output, position, 1)
  position = pdf_truetype_write_u16_at(output, position, 0)
  position = pdf_truetype_write_u32_at(output, position, 12)
  position = pdf_truetype_write_u16_at(output, position, 6)
  position = pdf_truetype_write_u16_at(
    output,
    position,
    10 + glyph_indices.length() * 2,
  )
  position = pdf_truetype_write_u16_at(output, position, 0)
  position = pdf_truetype_write_u16_at(output, position, 33)
  position = pdf_truetype_write_u16_at(output, position, glyph_indices.length())
  for glyph_index in glyph_indices {
    position = pdf_truetype_write_u16_at(
      output,
      position,
      pdf_truetype_normalize_u16(glyph_index),
    )
  }
  { length, bytes: Bytes::from_array(output), }
}

///|
fn pdf_truetype_sorted_tables(
  tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
) -> Array[@truetype.PdfTrueTypeTableRecord] {
  let sorted : Array[@truetype.PdfTrueTypeTableRecord] = []
  for table in tables {
    sorted.push(table)
  }
  sorted.sort_by(fn(left, right) { left.offset.compare(right.offset) })
  sorted
}

///|
fn pdf_truetype_subset_font_kept_tables(
  sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
) -> Array[@truetype.PdfTrueTypeTableRecord] {
  let kept : Array[@truetype.PdfTrueTypeTableRecord] = []
  let mut removed_data_before = 0
  for index in 0.. Int {
  length + pdf_truetype_table_padding(length)
}

///|
fn pdf_truetype_subset_original_table_span(
  data : BytesView,
  sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
  tag : String,
) -> (Int, Int) raise @core.PdfError {
  for index in 0.. data.length() {
        raise SoftError("Truncated TrueType " + tag + " table")
      }
      break (table.offset, length)
    }
  } nobreak {
    raise SoftError("No " + tag + " table found in TrueType font")
  }
}

///|
fn pdf_truetype_subset_original_table(
  data : BytesView,
  sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
  tag : String,
  directory_length : Int,
) -> @truetype.PdfTrueTypeSubsetTable raise @core.PdfError {
  let (offset, length) = pdf_truetype_subset_original_table_span(
    data, sorted_tables, tag,
  )
  { length: directory_length, bytes: data[offset:offset + length].to_owned(), }
}

///|
fn pdf_truetype_subset_font_table_payload(
  data : BytesView,
  sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
  record : @truetype.PdfTrueTypeTableRecord,
  subset_codepoints : ArrayView[Int],
  rewrite_cmap : Bool,
) -> @truetype.PdfTrueTypeSubsetTable raise @core.PdfError {
  if record.tag == "loca" {
    {
      length: record.length,
      bytes: pdf_truetype_subset_loca_table(data, subset_codepoints),
    }
  } else if record.tag == "glyf" {
    pdf_truetype_subset_glyf_table(data, subset_codepoints)
  } else if record.tag == "cmap" && rewrite_cmap {
    pdf_truetype_subset_cmap_table(data, subset_codepoints)
  } else {
    pdf_truetype_subset_original_table(
      data,
      sorted_tables,
      record.tag,
      record.length,
    )
  }
}

///|
fn pdf_truetype_subset_font_directory(
  data : BytesView,
  sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
  subset_codepoints : ArrayView[Int],
  rewrite_cmap : Bool,
) -> (
  Array[@truetype.PdfTrueTypeTableRecord],
  Array[@truetype.PdfTrueTypeSubsetTable],
) raise @core.PdfError {
  let kept = pdf_truetype_subset_font_kept_tables(sorted_tables)
  let header_size_reduction = 16 * (sorted_tables.length() - kept.length())
  let records : Array[@truetype.PdfTrueTypeTableRecord] = []
  let payloads : Array[@truetype.PdfTrueTypeSubsetTable] = []
  let mut glyf_table_size_reduction = 0
  let mut cmap_table_size_reduction = 0
  for kept_record in kept {
    let payload = pdf_truetype_subset_font_table_payload(
      data, sorted_tables, kept_record, subset_codepoints, rewrite_cmap,
    )
    let mut table_length = kept_record.length
    if kept_record.tag == "glyf" {
      table_length = payload.length
      glyf_table_size_reduction = pdf_truetype_table_padded_length(
          kept_record.length,
        ) -
        payload.bytes.length()
    } else if kept_record.tag == "cmap" && rewrite_cmap {
      table_length = payload.length
      cmap_table_size_reduction = pdf_truetype_table_padded_length(
          kept_record.length,
        ) -
        payload.bytes.length()
    }
    let prior_table_reduction = if kept_record.tag == "glyf" {
      cmap_table_size_reduction
    } else if kept_record.tag == "cmap" {
      glyf_table_size_reduction
    } else {
      glyf_table_size_reduction + cmap_table_size_reduction
    }
    records.push({
      tag: kept_record.tag,
      checksum: kept_record.checksum,
      offset: kept_record.offset - header_size_reduction - prior_table_reduction,
      length: table_length,
    })
    payloads.push(payload)
  }
  (records, payloads)
}

///|
/// Build a cpdf-style subset TrueType font file.
///
/// This ports the `subset_font` table-directory assembly around the already
/// ported `loca`, `glyf`, and format-6 `cmap` writers. Non-required TrueType
/// tables are removed, source checksums are preserved, table offsets are
/// adjusted by removed header/data spans and rewritten table sizes, and table
/// payloads are written in source table-order. Set `rewrite_cmap=true` for the
/// source `ImplicitInFontFile` higher-subset path; the default preserves the
/// original cmap table like the main subset path.
///
/// The `head` table is the source font's, bounding box included, as cpdf
/// writes it. Set `recompute_head=true` to write it as TTFunk (Prawn)
/// writes a subset's instead: the bounding box (`xMin`, `yMin`, `xMax`,
/// `yMax`) is the union of the bounding boxes in the headers of the glyphs
/// the subset keeps, simple and composite, every table's directory checksum
/// is recomputed from the subset's bytes, and `checkSumAdjustment` is set
/// so that the whole font sums to `0xB1B0AFBA`. A renderer may size its
/// glyph cache from the font's bounding box, so a subset carrying the full
/// font's box can rasterize differently from one carrying its own.
///
/// Only glyphs with an empty `loca` range are left out of the box. As an
/// exception kept for parity with TTFunk 1.7.0, a glyph that has data but
/// `numberOfContours == 0` still contributes the box stored in its header,
/// where OpenType's `head` box covers only glyphs with contours. The box is
/// `(0, 0, 0, 0)` when every kept glyph has an empty `loca` range. The
/// headers are trusted as stored: a box that underreports a glyph's
/// outline is not corrected.
pub fn pdf_truetype_subset_font(
  data : BytesView,
  subset_codepoints : ArrayView[Int],
  rewrite_cmap? : Bool = false,
  recompute_head? : Bool = false,
) -> @core.PdfBytes raise @core.PdfError {
  let major = pdf_truetype_required_u16(data, 0, "font header")
  let minor = pdf_truetype_required_u16(data, 2, "font header")
  let sorted_tables = pdf_truetype_sorted_tables(pdf_truetype_tables(data))
  let (records, payloads) = pdf_truetype_subset_font_directory(
    data, sorted_tables, subset_codepoints, rewrite_cmap,
  )
  let table_count = records.length()
  let search_power = pdf_truetype_power_two_leq(table_count)
  let mut output_length = 12 + table_count * 16
  for payload in payloads {
    output_length += payload.bytes.length()
  }
  let output = Array::make(output_length, b'\x00')
  let mut position = pdf_truetype_write_u16_at(output, 0, major)
  position = pdf_truetype_write_u16_at(output, position, minor)
  position = pdf_truetype_write_u16_at(output, position, table_count)
  position = pdf_truetype_write_u16_at(output, position, 16 * search_power)
  position = pdf_truetype_write_u16_at(
    output,
    position,
    pdf_truetype_source_entry_selector(search_power),
  )
  position = pdf_truetype_write_u16_at(output, position, table_count * 16)
  for record in records {
    position = pdf_truetype_write_tag_at(output, position, record.tag)
    position = pdf_truetype_write_u32_uint_at(output, position, record.checksum)
    position = pdf_truetype_write_u32_at(output, position, record.offset)
    position = pdf_truetype_write_u32_at(output, position, record.length)
  }
  for payload in payloads {
    position = pdf_truetype_write_view_at(output, position, payload.bytes)
  }
  if recompute_head {
    pdf_truetype_recompute_head(Bytes::from_array(output))
  } else {
    Bytes::from_array(output)
  }
}

///|
/// The checksum of `length` bytes of a font from `offset` (OpenType `head`
/// table, "Calculating Checksums"): the sum, modulo 2^32, of its big-endian
/// 32-bit words, a short last word padded with zeros.
fn pdf_truetype_checksum(
  data : ArrayView[Byte],
  offset : Int,
  length : Int,
) -> UInt {
  let end = offset + length
  let mut sum = 0U
  for word_start = offset; word_start < end; word_start = word_start + 4 {
    let mut word = 0U
    for index in word_start..<(word_start + 4) {
      let byte = if index < end && index < data.length() {
        data[index].to_uint()
      } else {
        0U
      }
      word = (word << 8) | byte
    }
    sum = sum + word
  }
  sum
}

///|
/// The union of the bounding boxes in the headers of a font's glyphs
/// (`xMin`, `yMin`, `xMax`, `yMax`), simple and composite alike, as TTFunk
/// 1.7.0 aggregates them: only a glyph with a zero-length `loca` range is
/// skipped, so a glyph with data but no contours still counts its stored
/// header box (OpenType would leave it out). A font whose glyphs are all
/// empty has the box `(0, 0, 0, 0)`.
fn pdf_truetype_glyph_bounds(
  data : BytesView,
) -> (Int, Int, Int, Int) raise @core.PdfError {
  let glyf = pdf_truetype_required_table(data, "glyf")
  let offsets = pdf_truetype_loca_offsets(data)
  let mut bounds : (Int, Int, Int, Int)? = None
  for index in 0..<(offsets.length() - 1) {
    let start = offsets[index]
    if offsets[index + 1] <= start {
      continue
    }
    let at = glyf.offset + start
    let x_min = pdf_truetype_required_i16(data, at + 2, "glyf table")
    let y_min = pdf_truetype_required_i16(data, at + 4, "glyf table")
    let x_max = pdf_truetype_required_i16(data, at + 6, "glyf table")
    let y_max = pdf_truetype_required_i16(data, at + 8, "glyf table")
    bounds = Some(
      match bounds {
        None => (x_min, y_min, x_max, y_max)
        Some((left, bottom, right, top)) =>
          (left.min(x_min), bottom.min(y_min), right.max(x_max), top.max(y_max))
      },
    )
  }
  bounds.unwrap_or((0, 0, 0, 0))
}

///|
/// Rewrite a font's `head` table as TTFunk's subsetter writes a subset's:
/// the bounding box is the union of the glyphs the font has outlines for,
/// every table's directory checksum is recomputed, and `checkSumAdjustment`
/// makes the whole font sum to `0xB1B0AFBA`.
fn pdf_truetype_recompute_head(
  font : Bytes,
) -> @core.PdfBytes raise @core.PdfError {
  let head = pdf_truetype_required_table(font, "head")
  if head.length < 54 || head.offset + 54 > font.length() {
    raise SoftError("Truncated TrueType head table")
  }
  let (x_min, y_min, x_max, y_max) = pdf_truetype_glyph_bounds(font)
  let output = font.to_array()
  // checkSumAdjustment is zero while the checksums are taken
  ignore(pdf_truetype_write_u32_uint_at(output, head.offset + 8, 0U))
  for index, value in [x_min, y_min, x_max, y_max] {
    ignore(
      pdf_truetype_write_u16_at(
        output,
        head.offset + 36 + index * 2,
        value & 0xFFFF,
      ),
    )
  }
  for index, record in pdf_truetype_tables(font) {
    ignore(
      pdf_truetype_write_u32_uint_at(
        output,
        12 + index * 16 + 4,
        pdf_truetype_checksum(output, record.offset, record.length),
      ),
    )
  }
  ignore(
    pdf_truetype_write_u32_uint_at(
      output,
      head.offset + 8,
      0xB1B0AFBAU - pdf_truetype_checksum(output, 0, output.length()),
    ),
  )
  Bytes::from_array(output)
}