///|
/// Error returned by font loaders and loader-backed cache helpers.
pub(all) enum FontLoadError {
  MissingFontData(String)
  FontFileReadFailed(String, String)
  FontParseFailed(String, FontParseError)
} derive(Eq, Debug)

///|
pub extend FontLoadError with Debug::{to_repr}

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

///|
/// Pluggable source of keyed font bytes.
pub(open) trait FontLoader {
  fn load_font_data(Self, String) -> Result[FontData, FontLoadError]
}

///|
/// One file-backed font source registered in a loader.
struct FontFileSource {
  path : String
  font_index : Int
} derive(Eq, Debug)

///|
pub extend FontFileSource with Debug::{to_repr}

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

///|
/// In-memory font loader for tests, embedded assets, and caller-owned bytes.
struct MemoryFontLoader {
  mut fonts : Map[String, FontData]
} derive(Debug)

///|
pub extend MemoryFontLoader with Debug::{to_repr}

///|
pub extend MemoryFontLoader with FontLoader::{load_font_data}

///|
/// File-backed font loader keyed by caller-provided resource names.
struct FileFontLoader {
  mut sources : Map[String, FontFileSource]
} derive(Debug)

///|
pub extend FileFontLoader with Debug::{to_repr}

///|
pub extend FileFontLoader with FontLoader::{load_font_data}

///|
/// Create a file-backed font source.
pub fn FontFileSource::new(
  path : String,
  font_index? : Int = 0,
) -> FontFileSource {
  { path, font_index, }
}

///|
/// Return the file path for this source.
pub fn FontFileSource::get_path(self : FontFileSource) -> String {
  self.path
}

///|
/// Return the face index inside the font file.
pub fn FontFileSource::get_font_index(self : FontFileSource) -> Int {
  self.font_index
}

///|
/// Create an empty in-memory font loader.
pub fn MemoryFontLoader::new() -> MemoryFontLoader {
  { fonts: Map([]), }
}

///|
/// Return the number of registered in-memory font entries.
pub fn MemoryFontLoader::len(self : MemoryFontLoader) -> Int {
  self.fonts.length()
}

///|
/// Return whether this loader has no registered font entries.
pub fn MemoryFontLoader::is_empty(self : MemoryFontLoader) -> Bool {
  self.len() is 0
}

///|
/// Return whether this loader has font data for a key.
pub fn MemoryFontLoader::contains(
  self : MemoryFontLoader,
  key : String,
) -> Bool {
  self.fonts.contains(key)
}

///|
/// Register or replace font data for a key.
pub fn MemoryFontLoader::insert(
  self : MemoryFontLoader,
  key : String,
  data : FontData,
) -> FontData {
  self.fonts[key] = data
  data
}

///|
/// Remove a registered in-memory font entry.
pub fn MemoryFontLoader::remove(self : MemoryFontLoader, key : String) -> Unit {
  self.fonts.remove(key)
}

///|
/// Drop all registered in-memory font entries.
pub fn MemoryFontLoader::clear(self : MemoryFontLoader) -> Unit {
  self.fonts = Map([])
}

///|
pub impl FontLoader for MemoryFontLoader with fn load_font_data(self, key) {
  match self.fonts.get(key) {
    Some(data) => Ok(data)
    None => Err(MissingFontData(key))
  }
}

///|
/// Create an empty file-backed font loader.
pub fn FileFontLoader::new() -> FileFontLoader {
  { sources: Map([]), }
}

///|
/// Return the number of registered file-backed font entries.
pub fn FileFontLoader::len(self : FileFontLoader) -> Int {
  self.sources.length()
}

///|
/// Return whether this loader has no registered file-backed font entries.
pub fn FileFontLoader::is_empty(self : FileFontLoader) -> Bool {
  self.len() is 0
}

///|
/// Return whether this loader has a source for a key.
pub fn FileFontLoader::contains(self : FileFontLoader, key : String) -> Bool {
  self.sources.contains(key)
}

///|
/// Register or replace a file source for a key.
pub fn FileFontLoader::insert(
  self : FileFontLoader,
  key : String,
  source : FontFileSource,
) -> FontFileSource {
  self.sources[key] = source
  source
}

///|
/// Remove a registered file source.
pub fn FileFontLoader::remove(self : FileFontLoader, key : String) -> Unit {
  self.sources.remove(key)
}

///|
/// Drop all registered file sources.
pub fn FileFontLoader::clear(self : FileFontLoader) -> Unit {
  self.sources = Map([])
}

///|
pub impl FontLoader for FileFontLoader with fn load_font_data(self, key) {
  match self.sources.get(key) {
    Some(source) => {
      let loaded : Bytes? = Some(read_file(source.path)) catch { _ => None }
      match loaded {
        Some(data) => Ok(FontData::new(data, font_index=source.font_index))
        None => Err(FontFileReadFailed(key, source.path))
      }
    }
    None => Err(MissingFontData(key))
  }
}

///|
/// Load font data from a generic loader.
pub fn[L : FontLoader] load_font_data(
  loader : L,
  key : String,
) -> Result[FontData, FontLoadError] {
  loader.load_font_data(key)
}

///|
/// Load and parse a checked font face from a generic loader.
pub fn[L : FontLoader] load_font_face(
  loader : L,
  key : String,
) -> Result[FontFace, FontLoadError] {
  match loader.load_font_data(key) {
    Err(error) => Err(error)
    Ok(data) =>
      match FontFace::from_data(data) {
        Ok(face) => Ok(face)
        Err(error) => Err(FontParseFailed(key, error))
      }
  }
}

///|
/// Load and parse a font face on first use, then reuse the cached face.
pub fn[L : FontLoader] FontFaceCache::get_or_load_result(
  self : FontFaceCache,
  key : String,
  loader : L,
) -> Result[FontFaceCacheParseResult, FontLoadError] {
  match self.get(key) {
    Some(face) => Ok({ face, did_parse: false, })
    None =>
      match loader.load_font_data(key) {
        Err(error) => Err(error)
        Ok(data) =>
          match FontFace::from_data(data) {
            Ok(face) => Ok({ face: self.insert(key, face), did_parse: true, })
            Err(error) => Err(FontParseFailed(key, error))
          }
      }
  }
}

///|
/// Load and parse a font face on first use, returning just the face.
pub fn[L : FontLoader] FontFaceCache::get_or_load(
  self : FontFaceCache,
  key : String,
  loader : L,
) -> Result[FontFace, FontLoadError] {
  match self.get_or_load_result(key, loader) {
    Ok(result) => Ok(result.get_face())
    Err(error) => Err(error)
  }
}