///|
// Deterministic font metrics for the layout engine. The bundled faces are
// parsed from the metrics-only sfnt data in data_*.mbt (regenerate with
// tools/font-metrics/gen_font_metrics.py); their glyph outlines, which only
// the PDF backend needs, live apart in `pagelayout/fontoutlines`.
//
// A caller's own faces come in through a `FontRegistry` (registry.mbt), an
// explicit value shared by whatever measures and whatever renders. The
// package-level functions here (`face`, `resolve_family`, `cjk_fallback`,
// `uncovered`) only ever see the bundled faces. The one process-wide state is
// the lazily filled cache of those bundled faces, which is written once, from
// constant data, and never replaced.

///|
/// Metrics of one face, bundled or registered. For a face parsed from an
/// sfnt the vertical metrics come from `hhea`; one built with
/// `FaceMetrics::new` carries what it was given (an AFM's, say). Advances
/// are raw font units scaled by `units_per_em` on lookup.
///
/// A face is immutable. Its glyph tables are private and read through
/// `glyph_id`, `cmap_entries` and `advance_pt`, because the same value is
/// shared by every lookup — the bundled cache, a `FontRegistry`, the layout
/// that measured with it and the renderer that draws with it — and a
/// change made through one of them would silently reach all the others.
pub struct FaceMetrics {
  family : String
  bold : Bool
  italic : Bool
  units_per_em : Int
  ascender : Int
  descender : Int
  line_gap : Int
  priv advances : Array[Int]
  priv cmap : Map[Int, Int]
}

///|
/// Why `FaceMetrics::new` refused its arguments.
pub(all) suberror FaceMetricsError {
  /// The metrics break an invariant every face keeps (see
  /// `FaceMetrics::new`). The payload names the family and the fault.
  InvalidFaceMetrics(String)
} derive(Eq)

///|
pub extend FaceMetricsError with Eq::{not_equal, equal}

///|
pub extend FaceMetricsError with Show::{to_string, output}

///|
pub impl Show for FaceMetricsError with fn output(self, logger) {
  match self {
    InvalidFaceMetrics(message) =>
      logger.write_string("invalid face metrics for \{message}")
  }
}

///|
/// Metrics given outright rather than parsed from an sfnt: for a face whose
/// metrics come from elsewhere, such as the AFM metrics of a PDF standard
/// font (see `FontRegistry::register_standard`). `advances` are indexed by
/// glyph, in units of `units_per_em`; `cmap` maps codepoints to glyphs.
/// Both tables are copied, so changing them afterwards cannot reach the
/// face.
///
/// Preconditions, checked (raising `FaceMetricsError` when one fails), so
/// that every measurement of the face is a finite number and no lookup can
/// fall outside a table:
///
/// - `units_per_em` is positive;
/// - `advances` is not empty, and no advance is negative;
/// - every `cmap` key is a codepoint (0 to 0x10FFFF), and every glyph it
///   maps to indexes `advances` (0 ≤ glyph < `advances.length()`).
///
/// A character the `cmap` lacks measures as glyph 0 (`.notdef`), as in any
/// face; `has_char` reports it missing. The vertical metrics are taken as
/// given (font units, `descender` negative below the baseline).
pub fn FaceMetrics::new(
  family~ : String,
  bold? : Bool = false,
  italic? : Bool = false,
  units_per_em~ : Int,
  ascender~ : Int,
  descender~ : Int,
  line_gap~ : Int,
  advances~ : Array[Int],
  cmap~ : Map[Int, Int],
) -> FaceMetrics raise FaceMetricsError {
  let invalid = fn(fault : String) -> FaceMetricsError {
    InvalidFaceMetrics("\{family}: \{fault}")
  }
  if units_per_em <= 0 {
    raise invalid("units_per_em \{units_per_em} is not positive")
  }
  if advances.is_empty() {
    raise invalid("no advances")
  }
  for glyph, advance in advances {
    if advance < 0 {
      raise invalid("glyph \{glyph} advances by \{advance}")
    }
  }
  for codepoint, glyph in cmap {
    if codepoint < 0 || codepoint > 0x10FFFF {
      raise invalid("\{codepoint} is not a codepoint")
    }
    if glyph < 0 || glyph >= advances.length() {
      raise invalid(
        "\{codepoint_label(codepoint)} maps to glyph \{glyph}, outside the \{advances.length()} advances",
      )
    }
  }
  {
    family,
    bold,
    italic,
    units_per_em,
    ascender,
    descender,
    line_gap,
    advances: advances.copy(),
    cmap: cmap.copy(),
  }
}

///|
/// `U+0041` and the like.
fn codepoint_label(codepoint : Int) -> String {
  "U+" + codepoint.to_string(radix=16).to_upper().pad_start(4, '0')
}

///|
/// The glyph `codepoint` maps to in the face's `cmap`, or None when it maps
/// to none. A mapping to glyph 0 (`.notdef`) is reported as it is; see
/// `has_char` for whether the face can really draw the character.
pub fn FaceMetrics::glyph_id(self : FaceMetrics, codepoint : Int) -> Int? {
  self.cmap.get(codepoint)
}

///|
/// Every (codepoint, glyph) pair of the face's `cmap`, in no particular
/// order. The pairs are read from the face; nothing done with them can
/// reach it.
pub fn FaceMetrics::cmap_entries(self : FaceMetrics) -> Iter2[Int, Int] {
  self.cmap.iter2()
}

///|
/// Horizontal advance of `codepoint` at `size_pt`, in points. Codepoints
/// missing from the face use the `.notdef` (glyph 0) advance; glyphs past
/// `numberOfHMetrics` share the table's final advance, per TrueType.
pub fn FaceMetrics::advance_pt(
  self : FaceMetrics,
  codepoint : Int,
  size_pt : Double,
) -> Double {
  let glyph = self.cmap.get(codepoint).unwrap_or(0)
  let index = if glyph >= self.advances.length() {
    self.advances.length() - 1
  } else {
    glyph
  }
  self.advances[index].to_double() * size_pt / self.units_per_em.to_double()
}

///|
/// Whether the face maps `codepoint` to a real glyph.
pub fn FaceMetrics::has_char(self : FaceMetrics, codepoint : Int) -> Bool {
  self.cmap.get(codepoint) is Some(glyph) && glyph != 0
}

///|
/// Baseline-to-top extent at `size_pt`, in points (positive).
pub fn FaceMetrics::ascent_pt(self : FaceMetrics, size_pt : Double) -> Double {
  self.ascender.to_double() * size_pt / self.units_per_em.to_double()
}

///|
/// Baseline-to-bottom extent at `size_pt`, in points (negative, per hhea).
pub fn FaceMetrics::descent_pt(self : FaceMetrics, size_pt : Double) -> Double {
  self.descender.to_double() * size_pt / self.units_per_em.to_double()
}

///|
/// Single line spacing at `size_pt`: ascender − descender + line gap.
pub fn FaceMetrics::line_height_pt(
  self : FaceMetrics,
  size_pt : Double,
) -> Double {
  (self.ascender - self.descender + self.line_gap).to_double() *
  size_pt /
  self.units_per_em.to_double()
}

///|
fn parse_face(
  family : String,
  bold : Bool,
  italic : Bool,
  chunks : Array[Bytes],
) -> FaceMetrics raise {
  let joined = Buffer()
  for chunk in chunks {
    joined.write_bytes(chunk)
  }
  let data : Bytes = @flate.pdf_flate_decode_view(joined.contents())
  parse_sfnt(family, bold, italic, data)
}

///|
fn parse_sfnt(
  family : String,
  bold : Bool,
  italic : Bool,
  data : Bytes,
) -> FaceMetrics raise {
  let metrics = @pdflite.pdf_truetype_metrics(data)
  let cmap : Map[Int, Int] = Map([])
  // later subtables replace earlier mappings, matching cpdf's reader
  for entry in @pdflite.pdf_truetype_cmap_glyphs(data) {
    cmap[entry.codepoint] = entry.glyph_index
  }
  guard @pdflite.pdf_truetype_table(data, "hhea") is Some(hhea) else {
    fail("face \{family} lacks an hhea table")
  }
  let ascender = read_i16(data, hhea.offset + 4)
  let descender = read_i16(data, hhea.offset + 6)
  let line_gap = read_i16(data, hhea.offset + 8)
  {
    family,
    bold,
    italic,
    units_per_em: metrics.units_per_em,
    ascender,
    descender,
    line_gap,
    advances: metrics.advance_widths,
    cmap,
  }
}

///|
fn read_i16(data : Bytes, offset : Int) -> Int {
  let hi = data[offset].to_int()
  let lo = data[offset + 1].to_int()
  let value = (hi << 8) | lo
  if value >= 0x8000 {
    value - 0x10000
  } else {
    value
  }
}

///|
let face_cache : Map[String, FaceMetrics] = Map([])

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

///|
fn load_faces() -> Unit {
  if face_cache.is_empty() {
    for entry in bundled_faces() {
      let (family, bold, italic, chunks) = entry
      let face = try! parse_face(family, bold, italic, chunks)
      face_cache[face_key(family, bold, italic)] = face
    }
  }
}

///|
/// Every bundled family, in registry order (deduplicated).
pub fn bundled_families() -> Array[String] {
  load_faces()
  let seen : Map[String, Unit] = Map([])
  let families : Array[String] = []
  for entry in bundled_faces() {
    let (family, _, _, _) = entry
    if !seen.contains(family) {
      seen[family] = ()
      families.push(family)
    }
  }
  families
}

///|
/// The bundled face for a *resolved* family name (see `resolve_family`).
/// Falls back to the family's regular face when the exact bold/italic face
/// is not bundled (Noto Sans SC ships regular only), then to Carlito. The
/// face returned says which one it is (`family`, `bold`, `italic`), so a
/// caller that needs the matching font program can ask for exactly that
/// face. For registered faces see `FontRegistry::face`.
pub fn face(
  family : String,
  bold? : Bool = false,
  italic? : Bool = false,
) -> FaceMetrics {
  load_faces()
  match face_cache.get(face_key(family, bold, italic)) {
    Some(found) => found
    None =>
      match face_cache.get(face_key(family, false, false)) {
        Some(regular) => regular
        None =>
          match face_cache.get(face_key("Carlito", bold, italic)) {
            Some(default_face) => default_face
            None => try! fail("bundled font registry is empty")
          }
      }
  }
}

///|
/// The face used for CJK codepoints the Latin faces lack.
pub fn cjk_fallback() -> FaceMetrics {
  face("Noto Sans SC")
}

///|
/// Codepoints in `text` that the face this run is set in cannot draw,
/// counted.
///
/// This asks the same question layout asks, and in the same order:
/// the requested face, then the CJK fallback. Asking the whole bundle
/// instead would under-report, because layout never reaches a third
/// family — U+05D0 lives in Liberation but not in Carlito or Noto Sans
/// SC, so Hebrew in a Carlito run is dropped even though *some* bundled
/// face has the glyph.
///
/// A character neither face covers is measured with `.notdef` and then
/// dropped by the backends: a simple font cannot encode it, and a
/// composite font would draw a blank box at the wrong width. That is a
/// silent loss worth reporting — a document whose evidence ratings or
/// equation variables are missing still renders, still validates, and is
/// still wrong.
pub fn uncovered(
  text : String,
  family : String,
  bold? : Bool = false,
  italic? : Bool = false,
) -> Array[(Int, Int)] {
  load_faces()
  uncovered_by(
    text,
    face(resolve_family(family), bold~, italic~),
    cjk_fallback(),
  )
}

///|
/// Codepoints in `text` that neither `requested` nor `fallback` can draw,
/// counted, in order of first appearance.
fn uncovered_by(
  text : String,
  requested : FaceMetrics,
  fallback : FaceMetrics,
) -> Array[(Int, Int)] {
  let counts : Map[Int, Int] = Map([])
  let order : Array[Int] = []
  let mut i = 0
  while i < text.length() {
    let unit = text[i].to_int()
    // decode surrogate pairs so astral characters report their real
    // codepoint rather than two halves
    let codepoint = if unit >= 0xD800 && unit <= 0xDBFF && i + 1 < text.length() {
      let low = text[i + 1].to_int()
      if low >= 0xDC00 && low <= 0xDFFF {
        i = i + 1
        0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00)
      } else {
        unit
      }
    } else {
      unit
    }
    i = i + 1
    // control characters are layout instructions, not glyphs
    if codepoint < 0x20 {
      continue
    }
    match counts.get(codepoint) {
      Some(n) => counts[codepoint] = n + 1
      None =>
        if !requested.has_char(codepoint) && !fallback.has_char(codepoint) {
          counts[codepoint] = 1
          order.push(codepoint)
        }
    }
  }
  [..order.map(fn(cp) { (cp, counts.get(cp).unwrap_or(0)) })]
}