// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Glyph atlas cache with LRU eviction.

use super::commands::AtlasCommandRecorder;
use super::key::GlyphCacheKey;
#[cfg(all(debug_assertions, feature = "std"))]
use super::key::SUBPIXEL_BUCKETS;
use super::region::{AtlasSlot, RasterMetrics};
use crate::Pixmap;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt::{Debug, Formatter};
use foldhash::fast::FixedState;
use hashbrown::HashMap;
use hashbrown::hash_map::RawEntryMut;
use smallvec::SmallVec;
pub use vello_common::image_cache::ImageCache;
pub use vello_common::multi_atlas::AtlasConfig;
use vello_common::paint::ImageId;

/// Deterministic hash map type alias.
///
/// Uses `foldhash::fast::FixedState` instead of the default random-seeded hasher
/// so that iteration order is identical across processes. This ensures that LRU
/// eviction deallocates atlas regions in a deterministic order, producing
/// reproducible atlas packing regardless of which binary (CPU / hybrid) runs.
type FixedHashMap<K, V> = HashMap<K, V, FixedState>;

/// Fixed seed for deterministic hashing across all glyph cache maps.
const HASH_SEED: u64 = 0;
/// Compile-time empty map for static (non-variable) glyph entries.
const EMPTY_GLYPH_MAP: FixedHashMap<GlyphCacheKey, GlyphCacheEntry> =
    FixedHashMap::with_hasher(FixedState::with_seed(HASH_SEED));
/// Compile-time empty map for variable-font glyph entries, keyed by variation coordinates.
const EMPTY_VAR_MAP: FixedHashMap<VarKey, FixedHashMap<GlyphCacheKey, GlyphCacheEntry>> =
    FixedHashMap::with_hasher(FixedState::with_seed(HASH_SEED));

/// Padding in pixels added to each side of a glyph to prevent texture bleeding.
///
/// The hybrid (GPU) renderer samples atlas sub-images via `Extend::Pad`, which
/// clamps out-of-bounds coordinates to the edge texel. Without at least 1px of
/// transparent padding, strip-rasteriser overshoot at glyph boundaries would
/// either duplicate the edge row/column or bleed in content from a neighbouring
/// glyph allocation. 1px is sufficient: the overshoot is sub-pixel, and the
/// transparent padding absorbs it. This padding also enables a future switch to
/// native bilinear sampling in the hybrid renderer.
pub const GLYPH_PADDING: u16 = 1;

/// Configuration for glyph cache behavior.
#[derive(Clone, Debug)]
pub struct GlyphCacheConfig {
    /// Maximum age (in frames) before an unused entry is evicted.
    pub max_entry_age: u64,
    /// How often (in frames) to run the eviction pass.
    pub eviction_frequency: u64,
    /// Maximum font size (in ppem) that will be cached. Glyphs rendered at
    /// sizes above this threshold are drawn directly each frame, since very
    /// large glyphs consume disproportionate atlas space.
    pub max_cached_font_size: f32,
}

impl Default for GlyphCacheConfig {
    fn default() -> Self {
        Self {
            max_entry_age: 64,
            eviction_frequency: 64,
            max_cached_font_size: 128.0,
        }
    }
}

/// A bitmap glyph pixmap awaiting GPU upload.
///
/// Accumulated during glyph encoding when a bitmap glyph is inserted into the
/// atlas cache. The application must drain these via
/// [`GlyphAtlas::drain_pending_uploads`] and upload each pixmap to the
/// GPU atlas at the position indicated by `image_id` (look up via
/// `ImageCache::get` to obtain atlas layer and offset).
#[derive(Debug)]
pub struct PendingBitmapUpload {
    /// The image ID allocated in the shared `ImageCache`.
    /// Use `image_cache.get(image_id)` to obtain `atlas_id` and `offset`.
    pub image_id: ImageId,
    /// The bitmap pixel data to upload.
    pub pixmap: Arc<Pixmap>,
    /// The atlas slot information for this glyph (includes dimensions).
    pub atlas_slot: AtlasSlot,
}

/// An atlas region that must be cleared to transparent.
///
/// Accumulated during eviction ([`GlyphAtlas::maintain`]) for every evicted
/// glyph. The application must drain these via
/// [`GlyphAtlas::drain_pending_clear_rects`] **after** calling `maintain` so
/// that freed atlas regions are zeroed before the slot is reused on a
/// subsequent frame. This prevents stale pixel data from bleeding through
/// when the renderer composites (`SrcOver`) onto the atlas.
#[derive(Clone, Copy, Debug)]
pub struct PendingClearRect {
    /// Which atlas page contains this region.
    pub page_index: u32,
    /// X position of the padded region in the atlas (pixels).
    pub x: u16,
    /// Y position of the padded region in the atlas (pixels).
    pub y: u16,
    /// Width of the padded region (pixels).
    pub width: u16,
    /// Height of the padded region (pixels).
    pub height: u16,
}

/// Core glyph atlas cache data shared by all renderer backends.
///
/// Contains the cache entries, LRU tracking, pending uploads, and statistics.
/// Does **not** own any pixel storage — that responsibility belongs to the
/// concrete wrapper types provided by the integrating renderer crate.
pub struct GlyphAtlas {
    /// Eviction configuration.
    eviction_config: GlyphCacheConfig,
    /// Entries for non-variable fonts.
    static_entries: FixedHashMap<GlyphCacheKey, GlyphCacheEntry>,
    /// Entries for variable fonts, keyed by variation coordinates.
    variable_entries: FixedHashMap<VarKey, FixedHashMap<GlyphCacheKey, GlyphCacheEntry>>,
    /// Current frame serial for LRU tracking.
    serial: u64,
    /// Serial of last eviction pass.
    last_eviction_serial: u64,
    /// Total cached glyph count (across all maps).
    entry_count: usize,
    /// Bitmap glyphs awaiting GPU upload.
    pending_uploads: Vec<PendingBitmapUpload>,
    /// Atlas regions that must be cleared to transparent before compositing.
    pending_clear_rects: Vec<PendingClearRect>,
    /// Outline and COLR glyph commands awaiting replay, indexed by atlas page.
    /// Uses `SmallVec` with inline capacity of 1 because most applications use
    /// a single atlas page; the common case avoids heap allocation entirely.
    pending_atlas_commands: SmallVec<[Option<AtlasCommandRecorder>; 1]>,
    /// Number of cache hits since last `clear_stats()`.
    cache_hits: u64,
    /// Number of cache misses since last `clear_stats()`.
    cache_misses: u64,
}

impl GlyphAtlas {
    /// Creates a new empty core cache with default eviction settings.
    pub fn new() -> Self {
        Self::with_config(GlyphCacheConfig::default())
    }

    /// Creates a new empty core cache with custom eviction settings.
    pub fn with_config(eviction_config: GlyphCacheConfig) -> Self {
        Self {
            eviction_config,
            static_entries: EMPTY_GLYPH_MAP,
            variable_entries: EMPTY_VAR_MAP,
            serial: 0,
            last_eviction_serial: 0,
            entry_count: 0,
            pending_uploads: Vec::new(),
            pending_clear_rects: Vec::new(),
            pending_atlas_commands: SmallVec::new(),
            cache_hits: 0,
            cache_misses: 0,
        }
    }

    /// Returns a reference to the cache configuration.
    pub fn config(&self) -> &GlyphCacheConfig {
        &self.eviction_config
    }

    /// Look up a cached glyph.
    pub fn get(&mut self, key: &GlyphCacheKey) -> Option<AtlasSlot> {
        let serial = self.serial;
        let entries = if key.var_coords.is_empty() {
            &mut self.static_entries
        } else {
            match self
                .variable_entries
                .raw_entry_mut()
                .from_key(&VarLookupKey(&key.var_coords))
            {
                RawEntryMut::Occupied(e) => e.into_mut(),
                RawEntryMut::Vacant(_) => {
                    self.cache_misses += 1;
                    return None;
                }
            }
        };

        match entries.get_mut(key) {
            Some(entry) => {
                entry.serial = serial;
                self.cache_hits += 1;
                Some(entry.atlas_slot)
            }
            None => {
                self.cache_misses += 1;
                None
            }
        }
    }

    /// Allocate atlas space and insert a cache entry.
    ///
    /// Returns the allocated [`AtlasSlot`] on success.
    #[expect(
        clippy::cast_possible_truncation,
        reason = "atlas offsets fit in u16 at reasonable atlas sizes"
    )]
    pub fn insert_entry(
        &mut self,
        image_cache: &mut ImageCache,
        key: GlyphCacheKey,
        raster_metrics: RasterMetrics,
    ) -> Option<AtlasSlot> {
        let padded_w = u32::from(raster_metrics.width) + u32::from(GLYPH_PADDING) * 2;
        let padded_h = u32::from(raster_metrics.height) + u32::from(GLYPH_PADDING) * 2;

        let image_id = image_cache.allocate(padded_w, padded_h, 0).ok()?;
        let resource = image_cache.get(image_id)?;
        let page_index = resource.atlas_id.as_u32() as usize;

        let x = resource.offset[0] + GLYPH_PADDING;
        let y = resource.offset[1] + GLYPH_PADDING;

        let atlas_slot = AtlasSlot {
            image_id,
            page_index: page_index as u32,
            x,
            y,
            width: raster_metrics.width,
            height: raster_metrics.height,
            bearing_x: raster_metrics.bearing_x,
            bearing_y: raster_metrics.bearing_y,
        };

        let entry = GlyphCacheEntry {
            atlas_slot,
            serial: self.serial,
        };

        let entries = if key.var_coords.is_empty() {
            &mut self.static_entries
        } else {
            match self
                .variable_entries
                .raw_entry_mut()
                .from_key(&VarLookupKey(&key.var_coords))
            {
                RawEntryMut::Occupied(e) => e.into_mut(),
                RawEntryMut::Vacant(e) => e.insert(key.var_coords.clone(), EMPTY_GLYPH_MAP).1,
            }
        };

        entries.insert(key, entry);
        self.entry_count += 1;

        Some(atlas_slot)
    }

    /// Allocate atlas space, insert a cache entry, and return the page recorder.
    #[expect(
        clippy::cast_possible_truncation,
        reason = "atlas dimensions are configured to fit in u16"
    )]
    pub fn insert(
        &mut self,
        image_cache: &mut ImageCache,
        key: GlyphCacheKey,
        raster_metrics: RasterMetrics,
    ) -> Option<(AtlasSlot, &mut AtlasCommandRecorder)> {
        let atlas_slot = self.insert_entry(image_cache, key, raster_metrics)?;
        let (atlas_w, atlas_h) = {
            let (w, h) = image_cache.atlas_manager().config().atlas_size;
            (w as u16, h as u16)
        };
        let recorder = self.recorder_for_page(atlas_slot.page_index, atlas_w, atlas_h);
        Some((atlas_slot, recorder))
    }

    /// Drain all pending bitmap uploads, keeping the allocation for reuse.
    pub fn drain_pending_uploads(&mut self) -> impl Iterator<Item = PendingBitmapUpload> + '_ {
        self.pending_uploads.drain(..)
    }

    /// Drain all pending clear rects, keeping the allocation for reuse.
    pub fn drain_pending_clear_rects(&mut self) -> impl Iterator<Item = PendingClearRect> + '_ {
        self.pending_clear_rects.drain(..)
    }

    /// Queue a bitmap pixmap for later processing.
    pub fn push_pending_upload(
        &mut self,
        image_id: ImageId,
        pixmap: Arc<Pixmap>,
        atlas_slot: AtlasSlot,
    ) {
        self.pending_uploads.push(PendingBitmapUpload {
            image_id,
            pixmap,
            atlas_slot,
        });
    }

    /// Replay all pending atlas command recorders (one per dirty page).
    ///
    /// The closure receives each non-empty recorder by mutable reference.
    /// After the closure returns, the recorder's commands are cleared but
    /// the allocation is kept for reuse next frame.
    pub fn replay_pending_atlas_commands(&mut self, mut f: impl FnMut(&mut AtlasCommandRecorder)) {
        for slot in &mut self.pending_atlas_commands {
            if let Some(recorder) = slot.as_mut()
                && !recorder.commands.is_empty()
            {
                f(recorder);
                recorder.commands.clear();
            }
        }
    }

    /// Get (or create) the command recorder for the given atlas page.
    pub fn recorder_for_page(
        &mut self,
        page_index: u32,
        atlas_width: u16,
        atlas_height: u16,
    ) -> &mut AtlasCommandRecorder {
        let idx = page_index as usize;
        if self.pending_atlas_commands.len() <= idx {
            self.pending_atlas_commands.resize_with(idx + 1, || None);
        }
        self.pending_atlas_commands[idx]
            .get_or_insert_with(|| AtlasCommandRecorder::new(page_index, atlas_width, atlas_height))
    }

    /// Advance the frame counter and potentially evict old entries.
    pub fn maintain(&mut self, image_cache: &mut ImageCache) {
        self.tick();
        let frames_since_eviction = self.serial - self.last_eviction_serial;
        if frames_since_eviction < self.eviction_config.eviction_frequency {
            return;
        }

        self.last_eviction_serial = self.serial;
        self.evict_old_entries(image_cache);
    }

    /// Advance the frame counter.
    fn tick(&mut self) {
        self.serial += 1;
    }

    /// Evict entries that haven't been used recently.
    ///
    /// For each evicted entry, queues a [`PendingClearRect`] covering the full
    /// padded atlas region. The application must drain these via
    /// [`drain_pending_clear_rects`](GlyphAtlas::drain_pending_clear_rects) and
    /// zero each region so that stale pixel data doesn't bleed through when
    /// the slot is later reused and composited with `SrcOver`.
    fn evict_old_entries(&mut self, image_cache: &mut ImageCache) {
        let serial = self.serial;
        let max_entry_age = self.eviction_config.max_entry_age;
        let entry_count = &mut self.entry_count;
        let pending_clear_rects = &mut self.pending_clear_rects;

        let mut should_retain = |entry: &GlyphCacheEntry| -> bool {
            let age = serial - entry.serial;
            if age > max_entry_age {
                image_cache.deallocate(entry.atlas_slot.image_id);
                *entry_count = entry_count.saturating_sub(1);
                push_clear_rect_for_slot(pending_clear_rects, &entry.atlas_slot);
                false
            } else {
                true
            }
        };

        self.static_entries.retain(|_, entry| should_retain(entry));

        self.variable_entries.retain(|_, entries| {
            entries.retain(|_, entry| should_retain(entry));
            !entries.is_empty()
        });
    }

    /// Clear all cache entries, pending work queues, and statistics.
    pub fn clear(&mut self) {
        self.static_entries.clear();
        self.variable_entries.clear();
        self.serial = 0;
        self.last_eviction_serial = 0;
        self.entry_count = 0;
        self.pending_uploads.clear();
        self.pending_clear_rects.clear();
        self.pending_atlas_commands.clear();
        self.cache_hits = 0;
        self.cache_misses = 0;
    }

    /// Get the number of cached glyphs.
    #[inline]
    pub fn len(&self) -> usize {
        self.entry_count
    }

    /// Returns `true` if the cache contains no entries.
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.entry_count == 0
    }

    /// Get the number of cache hits since last `clear_stats()`.
    #[inline]
    pub fn cache_hits(&self) -> u64 {
        self.cache_hits
    }

    /// Get the number of cache misses since last `clear_stats()`.
    #[inline]
    pub fn cache_misses(&self) -> u64 {
        self.cache_misses
    }

    /// Reset cache hit/miss counters without clearing the cache itself.
    pub fn clear_stats(&mut self) {
        self.cache_hits = 0;
        self.cache_misses = 0;
    }
}

/// Queue a clear rect covering the full padded region of an evicted atlas slot.
///
/// The slot's `x`/`y` are already inset by [`GLYPH_PADDING`] from the
/// allocation origin, so we subtract it back to get the padded top-left
/// corner and add `2 * GLYPH_PADDING` to each dimension.
fn push_clear_rect_for_slot(pending: &mut Vec<PendingClearRect>, slot: &AtlasSlot) {
    pending.push(PendingClearRect {
        page_index: slot.page_index,
        x: slot.x - GLYPH_PADDING,
        y: slot.y - GLYPH_PADDING,
        width: slot.width + 2 * GLYPH_PADDING,
        height: slot.height + 2 * GLYPH_PADDING,
    });
}

/// Statistics about cached glyphs.
#[cfg(all(debug_assertions, feature = "std"))]
#[derive(Debug)]
pub struct GlyphCacheStats {
    /// Number of glyphs from static (non-variable) fonts.
    pub static_glyphs: usize,
    /// Number of glyphs from variable fonts.
    pub variable_glyphs: usize,
    /// Number of atlas pages currently allocated.
    pub page_count: usize,
    /// Number of unique glyph IDs (same glyph may have multiple entries due to subpixel).
    pub unique_glyph_ids: usize,
    /// Distribution of entries across subpixel buckets.
    pub subpixel_distribution: [usize; SUBPIXEL_BUCKETS as usize],
    /// List of unique font sizes used.
    pub sizes_used: Vec<f32>,
}

#[cfg(all(debug_assertions, feature = "std"))]
impl GlyphCacheStats {
    /// Total number of cached glyph entries (static + variable).
    pub fn total_glyphs(&self) -> usize {
        self.static_glyphs + self.variable_glyphs
    }
}

#[cfg(all(debug_assertions, feature = "std"))]
impl GlyphAtlas {
    /// Get detailed statistics about cached glyphs.
    pub fn stats(&self, page_count: usize) -> GlyphCacheStats {
        use std::collections::HashSet;

        let mut unique_ids = HashSet::new();
        let mut subpixel_dist = [0; SUBPIXEL_BUCKETS as usize];
        let mut sizes = HashSet::new();

        for key in self.static_entries.keys() {
            unique_ids.insert(key.glyph_id);
            subpixel_dist[key.subpixel_x as usize] += 1;
            sizes.insert(key.size_bits);
        }

        let variable_count: usize = self.variable_entries.values().map(|m| m.len()).sum();

        for entries in self.variable_entries.values() {
            for key in entries.keys() {
                unique_ids.insert(key.glyph_id);
                subpixel_dist[key.subpixel_x as usize] += 1;
                sizes.insert(key.size_bits);
            }
        }

        GlyphCacheStats {
            static_glyphs: self.static_entries.len(),
            variable_glyphs: variable_count,
            page_count,
            unique_glyph_ids: unique_ids.len(),
            subpixel_distribution: subpixel_dist,
            sizes_used: sizes.into_iter().map(f32::from_bits).collect(),
        }
    }

    /// Log cache hit/miss statistics at debug level.
    pub fn log_hit_miss_stats(&self) {
        let total = self.cache_hits + self.cache_misses;
        let hit_rate = if total > 0 {
            (self.cache_hits as f64 / total as f64) * 100.0
        } else {
            0.0
        };
        log::debug!("=== Cache Hit/Miss Statistics ===");
        log::debug!("Cache hits:   {}", self.cache_hits);
        log::debug!("Cache misses: {}", self.cache_misses);
        log::debug!("Total lookups: {}", total);
        log::debug!("Hit rate:     {:.2}%", hit_rate);
    }

    /// Log detailed atlas statistics at debug level.
    pub fn log_atlas_stats(&self, page_count: usize) {
        let stats = self.stats(page_count);
        log::debug!("=== Glyph Atlas Statistics ===");
        log::debug!("Total cached glyphs: {}", stats.total_glyphs());
        log::debug!("Unique glyph IDs: {}", stats.unique_glyph_ids);
        log::debug!("Atlas pages: {}", stats.page_count);
        log::debug!("Static font glyphs: {}", stats.static_glyphs);
        log::debug!("Variable font glyphs: {}", stats.variable_glyphs);
        log::debug!("Subpixel distribution: {:?}", stats.subpixel_distribution);
        log::debug!("Font sizes: {:?}", stats.sizes_used);

        if stats.unique_glyph_ids > 0 {
            let ratio = stats.total_glyphs() as f32 / stats.unique_glyph_ids as f32;
            log::debug!("Avg entries per unique glyph: {:.2}", ratio);
        }
    }

    /// Returns all cached glyph keys (for debugging).
    pub fn all_keys(&self) -> impl Iterator<Item = &GlyphCacheKey> {
        self.static_entries
            .keys()
            .chain(self.variable_entries.values().flat_map(|e| e.keys()))
    }

    /// Log all cached keys grouped by glyph ID at debug level.
    ///
    /// This is useful for understanding why the same glyph appears multiple
    /// times in the atlas (e.g., different subpixel positions or sizes).
    pub fn log_keys_grouped(&self) {
        let mut by_glyph: HashMap<u32, Vec<(&GlyphCacheKey, &str)>> = HashMap::new();

        for key in self.static_entries.keys() {
            by_glyph
                .entry(key.glyph_id)
                .or_default()
                .push((key, "stat"));
        }
        for entries in self.variable_entries.values() {
            for key in entries.keys() {
                by_glyph
                    .entry(key.glyph_id)
                    .or_default()
                    .push((key, "var "));
            }
        }

        log::debug!(
            "=== Glyph Keys Grouped by ID ({} unique) ===",
            by_glyph.len()
        );

        let mut ids: Vec<_> = by_glyph.keys().copied().collect();
        ids.sort();

        for glyph_id in ids {
            let keys = &by_glyph[&glyph_id];
            let suffix = if keys.len() == 1 { "entry" } else { "entries" };
            log::debug!("glyph_id {:4} ({} {}):", glyph_id, keys.len(), suffix);
            for (k, source) in keys {
                log::debug!(
                    "    [{}] subpx: {}, size: {:.2}, hinted: {}, font_id: {:016x}, font_index: {}",
                    source,
                    k.subpixel_x,
                    f32::from_bits(k.size_bits),
                    k.hinted,
                    k.font_id,
                    k.font_index,
                );
            }
        }
    }
}

impl Default for GlyphAtlas {
    fn default() -> Self {
        Self::new()
    }
}

impl Debug for GlyphAtlas {
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("GlyphAtlas")
            .field("entry_count", &self.entry_count)
            .field("static_entries", &self.static_entries.len())
            .field("variable_fonts", &self.variable_entries.len())
            .field("serial", &self.serial)
            .finish_non_exhaustive()
    }
}

/// Internal cache entry storing atlas slot and access time.
struct GlyphCacheEntry {
    /// Atlas slot information for blitting.
    atlas_slot: AtlasSlot,
    /// Frame serial when last accessed (for LRU eviction).
    serial: u64,
}

/// Key for variable font caches (owned version).
type VarKey = SmallVec<[skrifa::instance::NormalizedCoord; 4]>;

/// Lookup key for variable font caches (borrowed version).
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
struct VarLookupKey<'a>(&'a [skrifa::instance::NormalizedCoord]);

impl hashbrown::Equivalent<VarKey> for VarLookupKey<'_> {
    fn equivalent(&self, other: &VarKey) -> bool {
        self.0 == other.as_slice()
    }
}

impl From<VarLookupKey<'_>> for VarKey {
    fn from(key: VarLookupKey<'_>) -> Self {
        Self::from_slice(key.0)
    }
}
