// Copyright (c) 2023-present, Raphael Amorim.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

use crate::sugarloaf::primitives::is_private_user_area;
use crate::SpanStyle;
use rustc_hash::FxHashMap;
use swash::Attributes;
use unicode_width::UnicodeWidthChar;

/// Unscaled horizontal advance for a glyph + the font's units-per-em,
/// stored together so callers can recover pixels at any font size:
/// `advance_units * font_size / units_per_em`. One cache entry survives
/// font-size changes.
#[derive(Debug, Clone, Copy)]
pub struct AdvanceInfo {
    pub advance_units: f32,
    pub units_per_em: u16,
}

impl AdvanceInfo {
    /// Scaled horizontal advance in pixels. Returns 0.0 when
    /// `units_per_em` is 0 (malformed/missing font metrics) so callers
    /// don't need a separate guard.
    #[inline]
    pub fn scaled(&self, font_size: f32) -> f32 {
        if self.units_per_em > 0 {
            self.advance_units * font_size / self.units_per_em as f32
        } else {
            0.0
        }
    }
}

/// Resolved glyph metadata: which font owns it, how wide it is in
/// terminal cells, whether it lives in a Unicode private-use area
/// (Nerd Font icons, custom symbol fonts, etc.), and — lazily — the
/// horizontal advance in the winning font's design units.
///
/// `advance` starts `None` and is filled only on the first
/// `char_advance` query for this `(char, attrs)`. The terminal grid
/// hot path doesn't need per-char pixel advances (it uses
/// `width` — cell count — for layout), so we don't pay for an hmtx /
/// upem read on every unique cell glyph.
#[derive(Debug, Clone, Copy)]
pub struct ResolvedGlyph {
    pub font_id: usize,
    pub width: f32,
    pub is_pua: bool,
    pub advance: Option<AdvanceInfo>,
}

/// Plain hash-map glyph cache. `get` is `&self` (no LRU promotion),
/// so callers can hold an immutable reference without borrow
/// conflicts. Unbounded — the working set of `(char, attrs)` pairs
/// in a terminal session is finite and small relative to memory.
pub(crate) struct FontCache {
    cache: FxHashMap<(char, Attributes), ResolvedGlyph>,
}

impl FontCache {
    pub fn new() -> Self {
        Self {
            cache: FxHashMap::default(),
        }
    }

    #[inline]
    pub fn get(&self, key: &(char, Attributes)) -> Option<&ResolvedGlyph> {
        self.cache.get(key)
    }

    #[inline]
    pub fn insert(&mut self, key: (char, Attributes), value: ResolvedGlyph) {
        self.cache.insert(key, value);
    }

    pub fn clear(&mut self) {
        self.cache.clear();
    }

    /// Attach `advance` to the entry for `key`, if one exists. Used by
    /// `Sugarloaf::char_advance` after the first pixel-advance query
    /// for a `(char, attrs)` pair so subsequent queries can short-circuit.
    /// No-op when the entry hasn't been resolved yet (caller should
    /// call `resolve_with` first).
    #[inline]
    pub(crate) fn set_advance(&mut self, key: (char, Attributes), advance: AdvanceInfo) {
        if let Some(entry) = self.cache.get_mut(&key) {
            entry.advance = Some(advance);
        }
    }
}

/// Resolve a single glyph: read from `cache` if present, otherwise
/// walk the fallback chain via `font_lib` and store the result.
///
/// `font_lib.resolve_font_for_char` does the registered-font walk
/// first; on a miss it falls back to platform cascade discovery
/// (CoreText `CTFontCreateForString` on macOS, fontconfig
/// `FcFontSort`+`FcCharSet` on Linux, font-kit `all_fonts` walk on
/// Windows) and registers the discovered font under a new `font_id`.
/// Subsequent queries for any codepoint that the discovered font
/// covers then hit the registered-font fast path directly.
pub(crate) fn resolve_with(
    cache: &mut FontCache,
    font_lib: &crate::font::FontLibrary,
    ch: char,
    attrs: Attributes,
) -> ResolvedGlyph {
    if let Some(cached) = cache.get(&(ch, attrs)) {
        return *cached;
    }

    let style = SpanStyle {
        font_attrs: attrs,
        ..Default::default()
    };
    let mut width = ch.width().unwrap_or(1) as f32;

    // Cross-platform: `resolve_font_for_char` does the fast-path
    // registered-font walk first, then falls back to platform cascade
    // discovery (CoreText on macOS, fontconfig on Linux, font-kit walk
    // on Windows) and registers the discovered font on first miss so
    // subsequent codepoints in the same script hit the fast path.
    // No route context here — this cache is for layout/width measurement,
    // not pane rendering. PUA codepoints fall through to the regular
    // font lookup, which returns a sensible width.
    let (font_id, is_emoji) = font_lib.resolve_font_for_char(ch, &style, None);

    if is_emoji {
        width = 2.0;
    }

    let resolved = ResolvedGlyph {
        font_id,
        width,
        is_pua: is_private_user_area(&ch),
        advance: None,
    };
    cache.insert((ch, attrs), resolved);
    resolved
}

/// Compute the unscaled glyph advance for `ch` in the font registered
/// under `font_id`. Returns `None` when the font data isn't available
/// (font id unregistered or the SFNT bytes failed to parse); the
/// caller is responsible for picking a rendering fallback.
#[cfg(not(target_os = "macos"))]
pub(crate) fn compute_advance(
    font_ctx: &crate::font::FontLibraryData,
    font_id: usize,
    ch: char,
) -> Option<AdvanceInfo> {
    let (data, offset, _key) = font_ctx.get_data(&font_id)?;
    let font_ref = swash::FontRef::from_index(&data, offset as usize)?;
    let glyph_id = font_ref.charmap().map(ch as u32);
    let metrics = font_ref.glyph_metrics(&[]);
    Some(AdvanceInfo {
        advance_units: metrics.advance_width(glyph_id),
        units_per_em: font_ref.metrics(&[]).units_per_em,
    })
}

/// macOS variant: derive the advance from CoreText without ever touching
/// the font's raw bytes. bytes-free font handling on
/// mac.
#[cfg(target_os = "macos")]
pub(crate) fn compute_advance(
    font_ctx: &crate::font::FontLibraryData,
    font_id: usize,
    ch: char,
) -> Option<AdvanceInfo> {
    let font = font_ctx.try_get(&font_id)?;
    let handle = if let Some(path) = font.path() {
        crate::font::macos::FontHandle::from_path(path)
    } else if let Some(bytes) = font.data() {
        crate::font::macos::FontHandle::from_bytes(bytes.as_ref())
    } else {
        None
    }?;
    let (advance_units, units_per_em) =
        crate::font::macos::advance_units_for_char(&handle, ch)?;
    Some(AdvanceInfo {
        advance_units,
        units_per_em,
    })
}
