///|
using @pagelayout {type Color}

///|
fn rgb_hex(value : Int) -> Color {
  Color::rgb((value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF)
}

///|
/// One face of the font catalog, with the metrics Prawn (the layout engine
/// behind Ruby asciidoctor-pdf) derives from it. Prawn works in 1/1000 em
/// and truncates every metric to an integer there; so does this.
priv struct Face {
  metrics : @fonts.FaceMetrics
  /// OS/2 typo metrics (hhea when zero), 1/1000 em, truncated
  ascender : Int
  descender : Int
  line_gap : Int
  /// `kern` table pairs, (left glyph << 16 | right glyph) → 1/1000 em
  kern : Map[Int, Double]
  /// a PDF standard font (Prawn's AFM fonts): its text is WinAnsi (see
  /// `winansi_text`), and its glyphs are WinAnsi codes
  standard : Bool
}

///|
/// Prawn's glyph width in 1/1000 em (truncated), used to break lines as
/// Prawn does.
fn Face::prawn_width(self : Face, codepoint : Int) -> Double {
  (self.metrics.advance_pt(codepoint, 1000.0) + 1.0e-9).floor()
}

///|
/// The width the PDF declares for the glyph (`/Widths`, truncated as Prawn
/// writes it: `render_pdf` gets `truncate_widths`), used to place glyphs,
/// so they sit where Prawn draws them and need no per-glyph displacement.
fn Face::drawn_width(self : Face, codepoint : Int) -> Double {
  self.prawn_width(codepoint)
}

///|
/// Kerning between two codepoints, 1/1000 em (negative tightens).
fn Face::kerning(self : Face, left : Int, right : Int) -> Double {
  if self.kern.is_empty() {
    return 0.0
  }
  let l = self.metrics.glyph_id(left).unwrap_or(0)
  let r = self.metrics.glyph_id(right).unwrap_or(0)
  self.kern.get((l << 16) | r).unwrap_or(0.0)
}

///|
fn Face::ascender_pt(self : Face, size : Double) -> Double {
  self.ascender.to_double() / 1000.0 * size
}

///|
/// Positive depth below the baseline.
fn Face::descender_pt(self : Face, size : Double) -> Double {
  -self.descender.to_double() / 1000.0 * size
}

///|
fn Face::line_gap_pt(self : Face, size : Double) -> Double {
  self.line_gap.to_double() / 1000.0 * size
}

///|
/// Prawn's `font.height`: ascender − descender + line gap.
fn Face::height_pt(self : Face, size : Double) -> Double {
  (self.ascender - self.descender + self.line_gap).to_double() / 1000.0 * size
}

///|
fn read_u16(data : Bytes, offset : Int) -> Int {
  (data[offset].to_int() << 8) | data[offset + 1].to_int()
}

///|
fn read_i16(data : Bytes, offset : Int) -> Int {
  let value = read_u16(data, offset)
  if value >= 0x8000 {
    value - 0x10000
  } else {
    value
  }
}

///|
/// Integer(x) in Ruby: truncation toward zero.
fn truncate(x : Double) -> Int {
  if x < 0.0 {
    -(-x).floor().to_int()
  } else {
    x.floor().to_int()
  }
}

///|
/// Parse the first subtable of a `kern` table when it is format 0: ttfunk,
/// and so Prawn, reads only that one.
fn parse_kern(data : Bytes, scale : Double) -> Map[Int, Double] {
  let pairs : Map[Int, Double] = Map([])
  guard @pdflite.pdf_truetype_table(data, "kern") is Some(table) else {
    return pairs
  }
  let base = table.offset
  // version 0 header: version, nTables; then the first subtable's version,
  // length, coverage (format in the high byte), nPairs and search fields
  guard read_u16(data, base) == 0 && read_u16(data, base + 2) > 0 else {
    return pairs
  }
  let subtable = base + 4
  guard read_u16(data, subtable + 4) >> 8 == 0 else { return pairs }
  let n = read_u16(data, subtable + 6)
  let mut p = subtable + 14
  for _ in 0.. Face raise {
  let metrics = registry.register(family, data, bold~, italic~)
  let scale = 1000.0 / metrics.units_per_em.to_double()
  // ttfunk: the OS/2 typo value when present and nonzero, else hhea
  let mut ascent = metrics.ascender
  let mut descent = metrics.descender
  let mut gap = metrics.line_gap
  match @pdflite.pdf_truetype_table(data, "OS/2") {
    Some(os2) => {
      let typo_ascent = read_i16(data, os2.offset + 68)
      let typo_descent = read_i16(data, os2.offset + 70)
      let typo_gap = read_i16(data, os2.offset + 72)
      if typo_ascent != 0 {
        ascent = typo_ascent
      }
      if typo_descent != 0 {
        descent = typo_descent
      }
      if typo_gap != 0 {
        gap = typo_gap
      }
    }
    None => ()
  }
  {
    metrics,
    ascender: truncate(ascent.to_double() * scale),
    descender: truncate(descent.to_double() * scale),
    line_gap: truncate(gap.to_double() * scale),
    kern: parse_kern(data, scale),
    standard: false,
  }
}

///|
/// The font catalog of the theme: family → the four styles, and the
/// pagelayout registry holding them, which the renderer draws with; and
/// the icon sets (`icons=font`), each a font family of its own.
pub struct FontCatalog {
  priv faces : Map[String, Face]
  priv registry : @fonts.FontRegistry
  /// icon set (`fas`) → icon name → glyph (see `IconSet`)
  priv icons : Map[String, Map[String, String]]
  /// Font Awesome 4 icon name (`fa-save`) → set and name (`far-save`)
  /// (asciidoctor-pdf's `fa-legacy-mapping.yml`)
  priv legacy_icons : Map[String, String]
  /// the theme's fallback fonts (`font_fallbacks`), in order
  priv fallbacks : Array[String]
}

///|
/// An icon font of prawn-icon (`data/fonts//`): the set's name (`fas`,
/// `far`, `fab`, `fi`, `pf`), its TrueType font and its legend, which names
/// each glyph: `name: glyph` lines (`fire: "…"`, under the set's key in
/// prawn-icon's `.yml`; the bundled `@icons.fonts()` have their own
/// tables, `/glyphs.txt`, made from the fonts' projects).
pub(all) struct IconSet {
  name : String
  font : Bytes
  legend : String
}

///|
/// A double-quoted YAML scalar's `\uXXXX` escapes decoded (prawn-icon's
/// `fi.yml` writes its glyphs so).
fn decode_unicode_escapes(value : String) -> String {
  guard value.contains("\\u") else { return value }
  let sb = StringBuilder()
  let chars = value.iter().collect()
  let mut i = 0
  let digit = (c : Char) => {
    match c {
      '0'..='9' => Some(c.to_int() - '0'.to_int())
      'a'..='f' => Some(c.to_int() - 'a'.to_int() + 10)
      'A'..='F' => Some(c.to_int() - 'A'.to_int() + 10)
      _ => None
    }
  }
  while i < chars.length() {
    if chars[i] == '\\' && i + 5 < chars.length() && chars[i + 1] == 'u' {
      let mut code = 0
      let mut valid = true
      for j in (i + 2)..<(i + 6) {
        match digit(chars[j]) {
          Some(d) => code = code * 16 + d
          None => valid = false
        }
      }
      if valid {
        sb.write_char(code.unsafe_to_char())
        i += 6
        continue
      }
    }
    sb.write_char(chars[i])
    i += 1
  }
  sb.to_string()
}

///|
/// The `key: value` lines of a flat YAML map, under its top-level key when
/// it has one (prawn-icon's legends: `fas:` then `  fire: "…"`; the legacy
/// mapping: `fa-save: far-save`). Keys starting with `__` are metadata.
fn parse_flat_yaml(text : String) -> Map[String, String] {
  let map : Map[String, String] = Map([])
  for raw in text.split("\n") {
    let line = raw.trim().to_owned()
    if line == "" || line.has_prefix("#") || line == "---" {
      continue
    }
    guard line.find(": ") is Some(i) else { continue }
    let key = line[:i].to_owned()
    if key.has_prefix("__") {
      continue
    }
    let mut value = line[i + 2:].trim().to_owned()
    if value.length() >= 2 && value.has_prefix("\"") && value.has_suffix("\"") {
      value = decode_unicode_escapes(value[1:value.length() - 1].to_owned())
    } else if value.length() >= 2 &&
      value.has_prefix("'") &&
      value.has_suffix("'") {
      value = value[1:value.length() - 1].to_owned()
    }
    map[key] = value
  }
  map
}

///|
fn face_key(family : String, bold : Bool, italic : Bool) -> String {
  "\{family}|\{bold}|\{italic}"
}

///|
/// The file names of the default theme's catalog (asciidoctor-pdf's
/// `data/fonts`): (family, bold, italic, file).
pub fn default_font_files() -> Array[(String, Bool, Bool, String)] {
  [
    ("Noto Serif", false, false, "notoserif-regular-subset.ttf"),
    ("Noto Serif", true, false, "notoserif-bold-subset.ttf"),
    ("Noto Serif", false, true, "notoserif-italic-subset.ttf"),
    ("Noto Serif", true, true, "notoserif-bold_italic-subset.ttf"),
    ("M+ 1mn", false, false, "mplus1mn-regular-subset.ttf"),
    ("M+ 1mn", true, false, "mplus1mn-bold-subset.ttf"),
    ("M+ 1mn", false, true, "mplus1mn-italic-subset.ttf"),
    ("M+ 1mn", true, true, "mplus1mn-bold_italic-subset.ttf"),
  ]
}

///|
/// The icon sets of prawn-icon 3.0.0 (its `data/fonts`): (set, font file,
/// legend file), relative to that directory.
pub fn default_icon_font_files() -> Array[(String, String, String)] {
  [
    ("fab", "fab/fa-brands.ttf", "fab/fab.yml"),
    ("far", "far/fa-regular.ttf", "far/far.yml"),
    ("fas", "fas/fa-solid.ttf", "fas/fas.yml"),
    ("fi", "fi/foundation-icons.ttf", "fi/fi.yml"),
    ("pf", "pf/paymentfont-webfont.ttf", "pf/pf.yml"),
  ]
}

///|
/// Load a catalog from font files: (family, bold, italic, TrueType bytes),
/// with the icon sets `icons=font` draws from, asciidoctor-pdf's mapping of
/// Font Awesome 4 icon names (`fa-legacy-mapping.yml`), and the families a
/// character the text's font lacks is looked for in (`fallbacks`).
pub fn FontCatalog::load(
  files : Array[(String, Bool, Bool, Bytes)],
  icon_sets? : Array[IconSet] = [],
  legacy_icon_mapping? : String = "",
  fallbacks? : Array[String] = [],
) -> FontCatalog raise {
  let faces : Map[String, Face] = Map([])
  let registry = @fonts.FontRegistry::new()
  for file in files {
    let (family, bold, italic, data) = file
    faces[face_key(family, bold, italic)] = load_face(
      registry, family, bold, italic, data,
    )
  }
  let icons : Map[String, Map[String, String]] = Map([])
  for set in icon_sets {
    faces[face_key(set.name, false, false)] = load_face(
      registry,
      set.name,
      false,
      false,
      set.font,
    )
    icons[set.name] = parse_flat_yaml(set.legend)
  }
  // the PDF standard fonts Prawn knows by family, unless the catalog has a
  // family of that name
  for entry in standard_families {
    let (family, names) = entry
    if faces.contains(face_key(family, false, false)) {
      continue
    }
    for i, name in names {
      let (bold, italic) = (i == 1 || i == 3, i >= 2)
      match load_standard_face(registry, family, bold, italic, name) {
        Some(face) => faces[face_key(family, bold, italic)] = face
        None => ()
      }
    }
  }
  // complete: nothing may join it once a document is measured with it
  registry.seal()
  {
    faces,
    registry,
    icons,
    legacy_icons: parse_flat_yaml(legacy_icon_mapping),
    fallbacks: fallbacks.filter(f => faces.contains(face_key(f, false, false))),
  }
}

///|
/// The text of fragments set in a standard font, in WinAnsi (see
/// `winansi_text`).
fn FontCatalog::apply_winansi(
  self : FontCatalog,
  fragments : Array[Fragment],
) -> Array[Fragment] {
  if !fragments.iter().any(f => self.is_standard(f.style)) {
    return fragments
  }
  fragments.map(f => {
    if f.text != "" && f.style.image < 0 && self.is_standard(f.style) {
      { ..f, text: winansi_text(f.text), }
    } else {
      f
    }
  })
}

///|
/// Whether a style's face is a standard font.
fn FontCatalog::is_standard(self : FontCatalog, style : Style) -> Bool {
  let (bold, italic) = style.face_style()
  self.face(style.family, bold, italic).standard
}

///|
/// Whether the face has a glyph for the character (Prawn's
/// `glyph_present?`).
fn Face::has_glyph(self : Face, codepoint : Int) -> Bool {
  match self.metrics.glyph_id(codepoint) {
    Some(gid) => gid != 0
    None => false
  }
}

///|
/// Prawn's fallback fonts (`process_fallback_fonts`): the characters of a
/// fragment its font lacks are set in the first fallback font that has
/// them (a character none has stays in the fragment's font, where it shows
/// as the .notdef glyph).
fn FontCatalog::apply_fallbacks(
  self : FontCatalog,
  fragments : Array[Fragment],
) -> Array[Fragment] {
  let fragments = self.apply_winansi(fragments)
  if self.fallbacks.is_empty() {
    return fragments
  }
  let out : Array[Fragment] = []
  for fragment in fragments {
    if fragment.text == "" || fragment.style.image >= 0 {
      out.push(fragment)
      continue
    }
    let (bold, italic) = fragment.style.face_style()
    let face = self.face(fragment.style.family, bold, italic)
    // the family of each character
    let family_of = (c : Char) => {
      let cp = c.to_int()
      if cp == 0x0A || face.has_glyph(cp) {
        return fragment.style.family
      }
      for f in self.fallbacks {
        if self.face(f, bold, italic).has_glyph(cp) {
          return f
        }
      }
      fragment.style.family
    }
    let sb = StringBuilder()
    let mut current = ""
    let mut first = true
    let flush = (family : String) => {
      if !sb.is_empty() {
        let style = if family == fragment.style.family {
          fragment.style
        } else {
          { ..fragment.style, family, font_set: true, bold, italic, }
        }
        out.push({ ..fragment, text: sb.to_string(), style, })
        sb.reset()
      }
    }
    for c in fragment.text {
      let family = family_of(c)
      if !first && family != current {
        flush(current)
      }
      current = family
      first = false
      sb.write_char(c)
    }
    flush(current)
  }
  out
}

///|
/// The glyph of icon `name` in icon set `set`, when the catalog has both.
fn FontCatalog::icon_glyph(
  self : FontCatalog,
  set : String,
  name : String,
) -> String? {
  self.icons.get(set).bind(glyphs => glyphs.get(name))
}

///|
/// Whether the catalog can draw font icons (it has icon sets).
fn FontCatalog::has_icons(self : FontCatalog) -> Bool {
  !self.icons.is_empty()
}

///|
/// The face text in a family neither the catalog nor the base font family
/// has is set in: Prawn's default font (Helvetica) in that style, reported
/// once per document (Prawn raises `UnknownFont`, which ends the
/// conversion).
fn FontCatalog::unknown_face(
  self : FontCatalog,
  family : String,
  bold : Bool,
  italic : Bool,
) -> Face {
  let style = match (bold, italic) {
    (false, false) => "normal"
    (true, false) => "bold"
    (false, true) => "italic"
    (true, true) => "bold_italic"
  }
  let known = session().unknown_fonts
  let key = "\{family} (\{style})"
  if !known.contains(key) {
    known[key] = true
    report(
      "WARNING \{key} is not a known font; using Helvetica instead at ",
    )
  }
  match self.faces.get(face_key("Helvetica", bold, italic)) {
    Some(face) => face
    None =>
      // the standard families are always there, unless the catalog has a
      // Helvetica of its own without that style
      match self.faces.get(face_key("Helvetica", false, false)) {
        Some(face) => face
        None => self.faces.values().next().unwrap()
      }
  }
}

///|
fn FontCatalog::face(
  self : FontCatalog,
  family : String,
  bold : Bool,
  italic : Bool,
) -> Face {
  match self.faces.get(face_key(family, bold, italic)) {
    Some(face) => face
    None =>
      match self.faces.get(face_key(family, false, false)) {
        Some(face) => face
        None =>
          match self.faces.get(face_key(base_font_family(), bold, italic)) {
            Some(face) => face
            None => self.unknown_face(family, bold, italic)
          }
      }
  }
}