//! Font loading and per-glyph metrics — thin wrapper over [`ab_glyph::FontVec`].
//!
//! The text system stays a layer above the font crate so it can swap
//! rasterizers later (an SDF generator, a different parser) without churning
//! the public Stage-8 API. Every text query a [`super::shape::shape`] or
//! [`super::atlas::GlyphAtlas`] call needs goes through [`Font`]'s methods —
//! `ab_glyph` is never visible to consumers of the engine.

use std::collections::HashMap;
use std::path::Path;

use ab_glyph::{Font as AbFont, FontVec, PxScale, ScaleFont};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use super::super::visual::FontRef;

/// Errors returned from font loading.
#[derive(Debug, Error)]
pub enum FontError {
    /// Reading the font file from disk failed.
    #[error("font file read failed: {0}")]
    Io(#[from] std::io::Error),
    /// The bytes were not a valid TTF / OTF font.
    #[error("not a valid TTF/OTF font")]
    InvalidFont,
}

/// Stable, opaque identifier for a font registered in a [`FontStore`].
///
/// Held in [`GlyphKey`](super::atlas::GlyphKey)s in the atlas and in
/// [`TextStyle`](super::shape::TextStyle)s passed to the shaper, so a font's
/// id never changes once registered. `Copy` + `Hash` so it indexes hash maps
/// cheaply.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FontId(pub u32);

/// One loaded font — a parsed TTF/OTF that can report metrics and rasterize
/// individual glyphs.
pub struct Font {
    inner: FontVec,
}

impl std::fmt::Debug for Font {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Font").finish_non_exhaustive()
    }
}

/// Result of rasterizing one glyph at a specific pixel size — the alpha mask
/// plus enough metrics to position it on a baseline.
#[derive(Debug, Clone, PartialEq)]
pub struct RasterizedGlyph {
    /// Width of the alpha mask in pixels.
    pub width: u32,
    /// Height of the alpha mask in pixels.
    pub height: u32,
    /// X offset from the glyph's pen position to the mask's left edge.
    pub bearing_x: f32,
    /// Y offset from the glyph's baseline to the mask's top edge (negative
    /// for glyphs that extend above the baseline, which is most of them).
    pub bearing_y: f32,
    /// How far to advance the pen along the baseline before the next glyph.
    pub advance_x: f32,
    /// Row-major alpha bytes (`width * height` bytes, `0 = transparent`,
    /// `255 = opaque`).
    pub bitmap: Vec<u8>,
}

impl Font {
    /// Parse a TTF/OTF font from raw bytes. Bytes are owned by the [`Font`].
    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, FontError> {
        FontVec::try_from_vec(bytes)
            .map(|inner| Self { inner })
            .map_err(|_| FontError::InvalidFont)
    }

    /// Load and parse a TTF/OTF file from disk.
    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, FontError> {
        let bytes = std::fs::read(path.as_ref())?;
        Self::from_bytes(bytes)
    }

    /// The glyph id for a `char`. Returns the font's `notdef` glyph (id `0`)
    /// for characters the font does not contain — same behavior as
    /// `ab_glyph`.
    pub fn glyph_id(&self, ch: char) -> GlyphId {
        GlyphId(self.inner.glyph_id(ch).0)
    }

    /// Horizontal advance for the next glyph at `size_px` logical pixels.
    pub fn h_advance_px(&self, glyph: GlyphId, size_px: f32) -> f32 {
        self.inner
            .as_scaled(PxScale::from(size_px))
            .h_advance(ab_glyph::GlyphId(glyph.0))
    }

    /// Ascender height in pixels at the given size.
    pub fn ascent_px(&self, size_px: f32) -> f32 {
        self.inner.as_scaled(PxScale::from(size_px)).ascent()
    }

    /// Descender depth in pixels at the given size. Negative for fonts where
    /// the descender sits below the baseline (the common case).
    pub fn descent_px(&self, size_px: f32) -> f32 {
        self.inner.as_scaled(PxScale::from(size_px)).descent()
    }

    /// Line gap in pixels — extra leading the font recommends between lines.
    pub fn line_gap_px(&self, size_px: f32) -> f32 {
        self.inner.as_scaled(PxScale::from(size_px)).line_gap()
    }

    /// Total recommended line height at `size_px` (ascent − descent +
    /// line_gap). Multiplied by `TextStyle`'s line-height factor by the
    /// shaper.
    pub fn line_height_px(&self, size_px: f32) -> f32 {
        let scaled = self.inner.as_scaled(PxScale::from(size_px));
        scaled.ascent() - scaled.descent() + scaled.line_gap()
    }

    /// Rasterize a single glyph to an alpha bitmap. Returns `None` for
    /// glyphs with no outline (e.g., the space character) — the caller still
    /// gets the advance via [`Font::h_advance_px`] and should treat the
    /// glyph as zero-area.
    pub fn rasterize(&self, glyph: GlyphId, size_px: f32) -> Option<RasterizedGlyph> {
        let scale = PxScale::from(size_px);
        let scaled = self.inner.as_scaled(scale);
        let advance_x = scaled.h_advance(ab_glyph::GlyphId(glyph.0));
        let mut positioned = ab_glyph::GlyphId(glyph.0).with_scale(scale);
        positioned.position = ab_glyph::point(0.0, 0.0);
        let outlined = self.inner.outline_glyph(positioned)?;
        let bounds = outlined.px_bounds();
        let width = bounds.width().ceil().max(1.0) as u32;
        let height = bounds.height().ceil().max(1.0) as u32;
        let mut bitmap = vec![0u8; (width as usize) * (height as usize)];
        outlined.draw(|x, y, coverage| {
            if x < width && y < height {
                let idx = (y as usize) * (width as usize) + (x as usize);
                bitmap[idx] = (coverage * 255.0).round().clamp(0.0, 255.0) as u8;
            }
        });
        Some(RasterizedGlyph {
            width,
            height,
            bearing_x: bounds.min.x,
            bearing_y: bounds.min.y,
            advance_x,
            bitmap,
        })
    }
}

/// [`AssetLoader`](crate::asset::AssetLoader) for TTF/OTF fonts.
///
/// Registered by default on every [`AssetServer`](crate::asset::AssetServer), so
/// a font file under a project's `assets/fonts/` can be loaded by path and an
/// [`AssetRef<Font>`](crate::asset::AssetRef) resolved to a [`Handle<Font>`](crate::asset::Handle)
/// — the link that lets the UI canvas pick a font asset and the runtime draw with it.
pub struct FontLoader;

impl crate::asset::AssetLoader for FontLoader {
    type Asset = Font;

    fn extensions(&self) -> &'static [&'static str] {
        &["ttf", "otf"]
    }

    fn load(&self, path: &Path) -> Result<Font, crate::asset::AssetError> {
        Font::from_path(path).map_err(|err| crate::asset::AssetError::Load {
            path: path.to_path_buf(),
            message: err.to_string(),
        })
    }
}

/// Opaque per-font glyph index. Mirrors `ab_glyph::GlyphId` but is the only
/// glyph type exposed by the engine, so consumers do not need an `ab_glyph`
/// dependency.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GlyphId(pub u16);

/// Registry of loaded fonts, indexed by [`FontId`] and (optionally) by
/// [`FontRef`] descriptor.
///
/// Why a descriptor index: piece-2 [`Theme`](super::super::theme::Theme)s
/// store fonts by family + weight + italic (`FontRef`), not by raw bytes.
/// `FontStore::resolve(&font_ref)` turns the descriptor into a [`FontId`] the
/// shaper can use, so a theme like `{ font: Some(FontRef::bold("Inter")) }`
/// works end-to-end as soon as the matching face has been registered.
#[derive(Default)]
pub struct FontStore {
    fonts: Vec<Font>,
    by_descriptor: HashMap<FontRef, FontId>,
}

impl std::fmt::Debug for FontStore {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("FontStore")
            .field("len", &self.fonts.len())
            .field("descriptors", &self.by_descriptor.len())
            .finish()
    }
}

impl FontStore {
    /// Create an empty store.
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a font with no descriptor — accessible only by its returned
    /// [`FontId`]. Useful for one-off uses where the font isn't part of a
    /// theme cascade.
    pub fn insert(&mut self, font: Font) -> FontId {
        let id = FontId(self.fonts.len() as u32);
        self.fonts.push(font);
        id
    }

    /// Register a font and associate it with a descriptor.
    ///
    /// Re-registering the same descriptor replaces the previous association
    /// but does not free the previous [`FontId`] — both ids continue to
    /// reference the now-distinct font. This matches Stage-7 `ActionMap`
    /// re-registration semantics: ids are stable, names can be remapped.
    pub fn insert_with_descriptor(&mut self, descriptor: FontRef, font: Font) -> FontId {
        let id = self.insert(font);
        self.by_descriptor.insert(descriptor, id);
        id
    }

    /// Look up a font by `FontId`.
    pub fn get(&self, id: FontId) -> Option<&Font> {
        self.fonts.get(id.0 as usize)
    }

    /// Resolve a [`FontRef`] descriptor (piece-2 theme value) to a
    /// [`FontId`], if the matching face has been registered.
    pub fn resolve(&self, descriptor: &FontRef) -> Option<FontId> {
        self.by_descriptor.get(descriptor).copied()
    }

    /// Number of registered fonts.
    pub fn len(&self) -> usize {
        self.fonts.len()
    }

    /// `true` if no fonts are registered.
    pub fn is_empty(&self) -> bool {
        self.fonts.is_empty()
    }
}

/// Common system paths a Linux-style host is likely to have a sans-serif
/// TTF at. Used by tests (and the eventual editor "no theme font set" path)
/// to find *some* font without bundling one.
///
/// Returned in priority order; the first existing path is the one to try.
/// Empty on hosts the search doesn't know about — the caller must handle
/// "no candidate found" gracefully.
pub fn common_system_font_paths() -> &'static [&'static str] {
    &[
        // Linux distributions:
        "/usr/share/fonts/liberation/LiberationSans-Regular.ttf",
        "/usr/share/fonts/TTF/DejaVuSans.ttf",
        "/usr/share/fonts/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/Adwaita/AdwaitaSans-Regular.ttf",
        // macOS:
        "/Library/Fonts/Arial.ttf",
        "/System/Library/Fonts/Helvetica.ttc",
    ]
}

/// Try to load a sans-serif font from a well-known system path. Returns
/// `None` (and prints `SKIP:`) if no candidate exists — the same pattern
/// the Stage-4 GPU tests use for "no adapter".
///
/// Test-only helper shared between the `font`, `atlas`, and `shape` modules
/// so the same "skip when no system font" branch isn't duplicated.
#[cfg(test)]
pub(crate) fn try_load_system_font() -> Option<Font> {
    for path in common_system_font_paths() {
        if Path::new(path).exists() {
            match Font::from_path(path) {
                Ok(font) => return Some(font),
                Err(err) => {
                    eprintln!("SKIP-candidate: {path} present but failed to load: {err}");
                }
            }
        }
    }
    eprintln!("SKIP: no system font available at any common Linux/macOS path");
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rejects_garbage_bytes() {
        let err = Font::from_bytes(vec![0u8; 32]).unwrap_err();
        assert!(matches!(err, FontError::InvalidFont));
    }

    #[test]
    fn missing_file_returns_io_error() {
        let err = Font::from_path("/nonexistent/font.ttf").unwrap_err();
        assert!(matches!(err, FontError::Io(_)));
    }

    #[test]
    fn font_loader_loads_through_the_asset_server() {
        use crate::asset::{AssetRef, AssetServer, AssetUid};

        // Find a real font file on disk; skip cleanly if the host has none.
        let Some(path) = common_system_font_paths()
            .iter()
            .map(std::path::Path::new)
            .find(|p| p.exists())
        else {
            eprintln!("SKIP: no system font path available");
            return;
        };

        // The default-registered FontLoader makes `.ttf`/`.otf` loadable.
        let server = AssetServer::new();
        let handle = server.load::<Font>(path);
        assert!(handle.is_loaded(), "font should load: {:?}", handle.error());
        // An asset reference to a hypothetical uid resolves to a handle when the
        // database hands back this path (proven in asset::database tests); here
        // we just confirm the loaded Font is usable.
        assert!(handle.get().unwrap().h_advance_px(GlyphId(0), 16.0) >= 0.0);
        // AssetRef<Font> is constructible (the field type the UI canvas uses).
        let _ = AssetRef::<Font>::new(AssetUid(1));
    }

    #[test]
    fn store_assigns_distinct_ids() {
        let Some(a) = try_load_system_font() else {
            return;
        };
        let Some(b) = try_load_system_font() else {
            return;
        };
        let mut store = FontStore::new();
        let id_a = store.insert(a);
        let id_b = store.insert(b);
        assert_ne!(id_a, id_b);
        assert_eq!(store.len(), 2);
        assert!(store.get(id_a).is_some());
        assert!(store.get(id_b).is_some());
        assert!(store.get(FontId(99)).is_none());
    }

    #[test]
    fn descriptor_resolves_to_registered_font() {
        let Some(font) = try_load_system_font() else {
            return;
        };
        let descriptor = FontRef::regular("System");
        let mut store = FontStore::new();
        let id = store.insert_with_descriptor(descriptor.clone(), font);
        assert_eq!(store.resolve(&descriptor), Some(id));
        // A different descriptor with no associated font is None.
        assert_eq!(store.resolve(&FontRef::bold("System")), None);
    }

    #[test]
    fn metrics_are_finite_and_non_zero() {
        let Some(font) = try_load_system_font() else {
            return;
        };
        let advance = font.h_advance_px(font.glyph_id('A'), 24.0);
        assert!(advance.is_finite());
        assert!(advance > 0.0);
        let ascent = font.ascent_px(24.0);
        let descent = font.descent_px(24.0);
        assert!(ascent > 0.0);
        // ab_glyph's `descent` is negative for descenders below the baseline.
        assert!(descent <= 0.0);
        assert!(font.line_height_px(24.0) > 0.0);
    }

    #[test]
    fn rasterize_produces_bitmap_for_solid_glyph() {
        let Some(font) = try_load_system_font() else {
            return;
        };
        let raster = font
            .rasterize(font.glyph_id('A'), 24.0)
            .expect("'A' outlines");
        assert!(raster.width > 0 && raster.height > 0);
        assert_eq!(
            raster.bitmap.len(),
            (raster.width as usize) * (raster.height as usize)
        );
        // A capital A at 24px should have at least one fully-opaque pixel
        // near its central stroke.
        assert!(raster.bitmap.iter().any(|&p| p > 200));
        // And some transparent pixels (it's not a solid square).
        assert!(raster.bitmap.iter().any(|&p| p < 10));
    }

    #[test]
    fn rasterize_space_returns_none_but_advance_works() {
        let Some(font) = try_load_system_font() else {
            return;
        };
        let space = font.glyph_id(' ');
        // Space has no outline — rasterize returns None.
        assert!(font.rasterize(space, 24.0).is_none());
        // But the advance is still positive so the shaper can lay it out.
        assert!(font.h_advance_px(space, 24.0) > 0.0);
    }

    #[test]
    fn common_system_font_paths_returns_some_candidates() {
        let paths = common_system_font_paths();
        assert!(!paths.is_empty());
        // Every entry should be an absolute path so the existence check is
        // unambiguous on the host.
        for p in paths {
            assert!(p.starts_with('/'), "{p:?} should be an absolute path");
        }
    }
}
