///|
// TrueType fonts (Prawn's `TTF`): families registered by their files
// (`font_families.update`), a file used as a font of its own, and the
// fallback fonts text looks in for what its font lacks. As in Prawn, a
// TrueType font is its file: the catalog has each file read as a face of
// its own, named by the file, read when first used (the catalog is made
// again with it, its registry taking no faces once used).

///|
/// Prawn's `font_families.update(family => { style => file })`: `family`
/// is set in `files` from now on, each the path of a TrueType font (read
/// through the document's `assets` when used).
pub fn Document::font_families_update(
  self : Document,
  family : String,
  files : Array[(FontStyle, String)],
) -> Unit {
  self.families[family] = files
}

///|
/// Prawn's `fallback_fonts`: the fonts (families, standard fonts, files) a
/// character text's font lacks is looked for in, in order.
pub fn Document::fallback_fonts(self : Document, fonts : Array[String]) -> Unit {
  self.fallbacks = fonts
}

///|
fn style_of_flags(flags : (Bool, Bool)) -> FontStyle {
  match flags {
    (false, false) => Normal
    (true, false) => Bold
    (false, true) => Italic
    (true, true) => BoldItalic
  }
}

///|
/// The file of `family` in `style`, when the family is one registered by
/// its files. Raises UnknownFont when the family has no such style.
fn Document::family_file(
  self : Document,
  family : String,
  style : FontStyle,
) -> String? raise PrawnError {
  guard self.families.get(family) is Some(files) else { return None }
  for entry in files {
    if entry.0.flags() == style.flags() {
      return Some(entry.1)
    }
  }
  let name = match style {
    Normal => "normal"
    Bold => "bold"
    Italic => "italic"
    BoldItalic => "bold_italic"
  }
  raise UnknownFont("\{family} (\{name}) is not a known font.")
}

///|
/// The TrueType font in `file`, read unless it is already; Prawn reads a
/// font when it is first used. A file that cannot be read is Prawn's
/// ArgumentError; one not a TrueType font, not yet supported. The face
/// joins the catalog only once the catalog is made with it.
fn Document::load_file(self : Document, file : String) -> Unit raise PrawnError {
  if self.font_files.iter().any(f => f.0 == file) {
    return
  }
  guard (self.assets)(file) is Some(bytes) else {
    raise ArgumentError("\{file} not found")
  }
  if !file.to_lower().has_suffix(".ttf") {
    raise Unsupported("the font \{file} (only TrueType is read yet)")
  }
  let files = [..self.font_files, (file, bytes)]
  let catalog = FontCatalog::load(files.map(f => (f.0, false, false, f.1))) catch {
    e => raise Unsupported("the font \{file}: \{e}")
  }
  self.font_files = files
  self.catalog = catalog
  self.text_context = { catalog, hooks: self.text_context.hooks, }
}

///|
/// The font `name` is in `flags` ready to set text in: a family's face in
/// that style (UnknownFont when it lacks it) or a font file, read as
/// needed; a standard font needs nothing.
fn Document::ensure_font(
  self : Document,
  name : String,
  flags : (Bool, Bool),
) -> Unit raise PrawnError {
  if is_font_file(name) {
    self.load_file(name)
  } else {
    match self.family_file(name, style_of_flags(flags)) {
      Some(file) => self.load_file(file)
      None => ()
    }
  }
}

///|
/// The catalog's face for font `name` in `flags`: a family registered by
/// its files is its file in that style; a file, itself; a standard family,
/// itself in that style. (family, bold, italic) as the catalog keys it.
fn Document::catalog_key(
  self : Document,
  name : String,
  flags : (Bool, Bool),
) -> (String, Bool, Bool) {
  match self.families.get(name) {
    Some(files) => {
      for entry in files {
        if entry.0.flags() == flags {
          return (entry.1, false, false)
        }
      }
      (name, flags.0, flags.1)
    }
    None if is_font_file(name) => (name, false, false)
    None => (name, flags.0, flags.1)
  }
}

///|
/// The face the catalog keys so.
fn Document::face_by_key(self : Document, key : (String, Bool, Bool)) -> Face {
  self.text_context.face(key.0, key.1, key.2)
}

///|
/// Prawn's `glyph_present?`: a standard font has a character it can encode
/// (a line break included); a TrueType font, one its cmap maps.
fn glyph_present(face : Face, cp : Int) -> Bool {
  if face.standard {
    cp < 0x80 || winansi_code_of(cp) is Some(_)
  } else {
    face.has_glyph(cp)
  }
}

///|
/// A fragment's own `:font` and styles (Prawn's fragment hash), which its
/// fallback fonts are looked up with.
fn Document::own_font(
  self : Document,
  style : Style,
) -> (String?, (Bool, Bool)) {
  match self.fragment_meta.get(style.callback) {
    Some(meta) => (meta.font, meta.styles)
    None => (None, (false, false))
  }
}

///|
/// The face font `name` stands for in the fragment's own styles `flags`,
/// found as Prawn's `font(name, style:)` finds it (read as needed); None
/// for the current font as it is (a current font of no family).
fn Document::fallback_face(
  self : Document,
  name : String?,
  flags : (Bool, Bool),
) -> Face raise PrawnError {
  match name {
    None => self.face_by_key(self.font_key)
    Some(name) => {
      let (family, face_flags, _) = self.find_font(name, style_of_flags(flags))
      self.face_by_key(self.catalog_key(family, face_flags))
    }
  }
}

///|
/// Prawn's `find_font_for_this_glyph`: the font character `cp` of a
/// fragment in `style` is set in, by name. The fragment's font (its own
/// `:font`, else the current font's family; None when that has no family)
/// when it has the character, else the first fallback that has it, else
/// the fragment's font, each looked up in the fragment's own styles and
/// read only when the character reaches it. None also when it is the
/// fragment's font and the fragment named none: it keeps its font.
fn Document::glyph_font(
  self : Document,
  style : Style,
  cp : Int,
) -> String? raise PrawnError {
  let (own, flags) = self.own_font(style)
  let fragment_font = match own {
    Some(font) => Some(font)
    None if self.font_family_known => Some(self.family)
    None => None
  }
  let candidates = [..self.fallbacks.map(name => Some(name)), fragment_font]
  let mut chosen = fragment_font
  if !glyph_present(self.fallback_face(fragment_font, flags), cp) {
    for i, candidate in candidates {
      chosen = candidate
      if i == candidates.length() - 1 ||
        glyph_present(self.fallback_face(candidate, flags), cp) {
        break
      }
    }
  }
  if own is None && chosen == fragment_font {
    None
  } else {
    chosen
  }
}

///|
/// The face character `cp` of a fragment in `style` is set in (Prawn's
/// fallback processing).
fn Document::glyph_face(
  self : Document,
  style : Style,
  cp : Int,
) -> Face raise PrawnError {
  match self.glyph_font(style, cp) {
    None => self.face_of(style)
    Some(name) => self.fallback_face(Some(name), self.own_font(style).1)
  }
}

///|
/// Prawn's fallback processing (`process_fallback_fonts`): each fragment
/// split where the font its characters are set in changes; a piece in
/// another font is set in it, in the fragment's own styles, and is drawn
/// in it (callbacks see it current).
fn Document::split_fallbacks(
  self : Document,
  fragments : Array[Fragment],
) -> Array[Fragment] raise PrawnError {
  guard !self.fallbacks.is_empty() else { return fragments }
  let out : Array[Fragment] = []
  for fragment in fragments {
    if fragment.text == "" || fragment.style.image >= 0 {
      out.push(fragment)
      continue
    }
    let flags = self.own_font(fragment.style).1
    let sb = StringBuilder()
    let mut current : String? = None
    let mut first = true
    let flush = (font : String?) => {
      if !sb.is_empty() {
        let style = match font {
          None => fragment.style
          Some(name) => {
            // a piece with a font of its own needs a current font with a
            // family (Prawn's arranger)
            if !self.font_family_known {
              raise BadFontFamily(
                "Prawn::Text::Formatted::Arranger::BadFontFamily",
              )
            }
            let (family, face_flags, known) = self.find_font(
              name,
              style_of_flags(flags),
            )
            let key = self.catalog_key(family, face_flags)
            // its own record: the font it is drawn in, current while it is
            let meta = self.fragment_meta.length()
            match self.fragment_meta.get(fragment.style.callback) {
              Some(m) =>
                self.fragment_meta.push({
                  ..m,
                  font_name: family,
                  font_flags: face_flags,
                  family_known: Some(known),
                  font: Some(name),
                })
              // (a text box's text has none: a record of its own)
              None =>
                self.fragment_meta.push({
                  color_set: false,
                  font_name: family,
                  font_flags: face_flags,
                  styles: flags,
                  font: Some(name),
                  family_known: Some(known),
                  anchor: None,
                  callbacks: [],
                })
            }
            {
              ..fragment.style,
              family: key.0,
              bold: key.1,
              italic: key.2,
              font_set: true,
              callback: meta,
            }
          }
        }
        out.push({ ..fragment, text: sb.to_string(), style, })
        sb.reset()
      }
    }
    for c in fragment.text {
      let font = self.glyph_font(fragment.style, c.to_int())
      if !first && font != current {
        flush(current)
      }
      current = font
      first = false
      sb.write_char(c)
    }
    flush(current)
  }
  out
}

///|
/// Whether a font name is a file (Prawn tells by the extension).
fn is_font_file(name : String) -> Bool {
  let lower = name.to_lower()
  [".ttf", ".otf", ".dfont", ".ttc"].iter().any(ext => lower.has_suffix(ext))
}