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

//! Gradient ramp cache for `vello_hybrid` renderer.

use alloc::vec::Vec;
use hashbrown::HashMap;
use vello_common::encode::{EncodedGradient, GradientCacheKey};
use vello_common::fearless_simd::{Level, Simd, dispatch};
use vello_common::peniko::color::cache_key::CacheKey;

/// Number of bytes per texel in the gradient texture.
/// Gradient textures use `Rgba8Unorm` format (4 bytes per texel).
/// This constant is used to convert between byte offsets and texel indices.
const BYTES_PER_TEXEL: u32 = 4;

#[derive(Debug)]
pub(crate) struct GradientRampCache {
    /// Current epoch for LRU tracking.
    epoch: u64,
    /// Cache mapping gradient signature to cached ramps and last access time.
    cache: HashMap<CacheKey<GradientCacheKey>, (CachedRamp, u64)>,
    /// Packed gradient luts.
    luts: Vec<u8>,
    /// Whether the packed luts needs to be re-uploaded.
    has_changed: bool,
    /// Maximum number of gradient cache entries to retain.
    retained_count: u32,
    /// SIMD level used for gradient LUT generation.
    level: Level,
    /// Scratch space for maintaining the cache.
    scratch: ScratchSpace,
}

/// Reusable working memory for cache maintenance operations.
///
/// This struct implements a memory pool pattern to avoid repeated allocations
/// during cache eviction and compaction. Each vector is borrowed during
/// operations and returned with its capacity preserved for future reuse.
#[derive(Debug, Default)]
struct ScratchSpace {
    /// Temporary storage for cache entries during LRU sorting.
    /// Holds (`key_reference`, `last_used_timestamp`) pairs for all cache entries.
    entries: Vec<(&'static CacheKey<GradientCacheKey>, u64)>,
    /// Temporary storage for cache keys that need to be removed.
    /// Collects the keys of the LRU entries before removal.
    removed: Vec<CacheKey<GradientCacheKey>>,
    /// Temporary storage for evicted cache entries and their ramp data.
    /// Contains (`key`, `ramp`) pairs for entries that have been removed from the cache.
    /// Used during LUT compaction to know which ramp data to remove from the packed LUTs.
    lru_entries: Vec<(CacheKey<GradientCacheKey>, CachedRamp)>,
    /// Scratch buffer for computing prefix sums during LUT compaction.
    prefix_sum: Vec<u32>,
}

impl GradientRampCache {
    /// Create a new gradient ramp cache with the specified retained count.
    pub(crate) fn new(retained_count: u32, level: Level) -> Self {
        Self {
            epoch: 0,
            cache: HashMap::new(),
            luts: Vec::new(),
            has_changed: false,
            retained_count,
            level,
            scratch: ScratchSpace::default(),
        }
    }

    /// Get or generate a gradient ramp, returning its offset in the packed luts.
    #[allow(
        clippy::cast_possible_truncation,
        reason = "Conversion from usize to u32 is safe, used for texture coordinates"
    )]
    pub(crate) fn get_or_create_ramp(&mut self, gradient: &EncodedGradient) -> (u32, u32) {
        self.epoch += 1;

        // Check if we already have this gradient cached.
        if let Some((cached_ramp, last_used)) = self.cache.get_mut(&gradient.cache_key) {
            *last_used = self.epoch;
            return (cached_ramp.lut_start, cached_ramp.width);
        }

        // Generate new gradient LUT.
        let lut_start = self.luts.len() as u32 / BYTES_PER_TEXEL;
        let width = dispatch!(self.level, simd => generate_gradient_lut_impl(simd, gradient, &mut self.luts))
            as u32;
        let cached_ramp = CachedRamp { width, lut_start };
        self.has_changed = true;
        self.cache
            .insert(gradient.cache_key.clone(), (cached_ramp, self.epoch));

        (lut_start, width)
    }

    /// Maintain the gradient cache by evicting old entries.
    pub(crate) fn maintain(&mut self) {
        let entries_to_remove_count = self
            .cache
            .len()
            .saturating_sub(self.retained_count as usize);
        self.evict(entries_to_remove_count);
    }

    /// Get the size of the packed luts.
    pub(crate) fn luts_size(&self) -> usize {
        self.luts.len()
    }

    /// Check if the packed luts is empty.
    pub(crate) fn is_empty(&self) -> bool {
        self.luts.is_empty()
    }

    /// Check if the luts data has changed.
    pub(crate) fn has_changed(&self) -> bool {
        self.has_changed
    }

    /// Mark the luts as synced.
    pub(crate) fn mark_synced(&mut self) {
        self.has_changed = false;
    }

    /// Take ownership of the luts, leaving an empty vector in its place.
    pub(crate) fn take_luts(&mut self) -> Vec<u8> {
        core::mem::take(&mut self.luts)
    }

    /// Restore the luts. The restored luts should have the same logical content as the original.
    pub(crate) fn restore_luts(&mut self, luts: Vec<u8>) {
        self.luts = luts;
    }

    /// Remove multiple LRU entries and compact the LUTs vector.
    fn evict(&mut self, count: usize) {
        if count == 0 {
            return;
        }

        let mut lru_entries = core::mem::take(&mut self.scratch.lru_entries);
        let mut prefix_sum = core::mem::take(&mut self.scratch.prefix_sum);
        lru_entries.clear();
        self.remove_lru_entries(count, &mut lru_entries);
        self.compact_luts(&mut lru_entries, &mut prefix_sum);
        self.scratch.lru_entries = lru_entries;
        self.scratch.prefix_sum = prefix_sum;
        self.has_changed = true;
    }

    /// Identify and remove the LRU cache entries.
    fn remove_lru_entries(
        &mut self,
        count: usize,
        lru_entries: &mut Vec<(CacheKey<GradientCacheKey>, CachedRamp)>,
    ) {
        if count == 0 || self.cache.is_empty() {
            return;
        }

        let mut entries = reuse_vec(core::mem::take(&mut self.scratch.entries));
        entries.extend(
            self.cache
                .iter()
                .map(|(key, (_, last_used))| (key, *last_used)),
        );

        let (lesser, median, _) =
            entries.select_nth_unstable_by_key(count - 1, |(_, last_used)| *last_used);

        // Note that since we use `select_nth_unstable`, the entries themselves are not guaranteed
        // to be sorted.
        let mut removed = core::mem::take(&mut self.scratch.removed);
        removed.clear();
        removed.extend(lesser.iter().map(|(key, _)| (*key).clone()));
        removed.push(median.0.clone());
        self.scratch.entries = reuse_vec(entries);

        for key in removed.drain(..) {
            let ramp = self.cache.remove(&key).unwrap().0;
            lru_entries.push((key, ramp));
        }

        self.scratch.removed = removed;
    }

    /// Remove LUT data for evicted entries with compacting the LUTs vector, and update remaining offsets.
    fn compact_luts(
        &mut self,
        ramps_to_remove: &mut [(CacheKey<GradientCacheKey>, CachedRamp)],
        prefix_sum: &mut Vec<u32>,
    ) {
        if ramps_to_remove.is_empty() {
            return;
        }

        // See comment in `remove_lru_entries`, the entries are not sorted yet but need to be
        // for the prefix sum to work correctly.
        ramps_to_remove.sort_by_key(|(_, ramp)| ramp.lut_start);

        // Compute a prefix sum of how much the `lut_start` entry of a given cached ramp needs
        // to be adjusted to account for compaction.
        prefix_sum.clear();
        prefix_sum.push(0);
        let mut write_pos = 0;
        let mut read_pos = 0;

        for (_, ramp) in ramps_to_remove.iter() {
            let remove_start = (ramp.lut_start * BYTES_PER_TEXEL) as usize;
            let remove_end = remove_start + (ramp.width * BYTES_PER_TEXEL) as usize;
            // First, copy all the LUT entries before the removed entry to the new
            // write position.
            if read_pos < remove_start {
                self.luts.copy_within(read_pos..remove_start, write_pos);
                write_pos += remove_start - read_pos;
            }

            // Update the read position as well as the prefix sum.
            read_pos = remove_end;
            prefix_sum.push(prefix_sum.last().unwrap() + ramp.width);
        }

        // Handle the tail if it exists.
        let luts_len = self.luts.len();
        if read_pos < luts_len {
            self.luts.copy_within(read_pos..luts_len, write_pos);
            write_pos += luts_len - read_pos;
        }
        self.luts.truncate(write_pos);

        // For each entry that is still in the cache, find the correct index in the prefix sum
        // and adjust the start position.
        for (_, (ramp, _)) in self.cache.iter_mut() {
            // This partition point will yield the position of the first entry where `r.lut_start >= ramp.lut_start`,
            // so that entry shouldn't be included anymore. However, since `prefix_sum` starts with
            // a `0` entry, this index is shifted by one and thus the correct one.
            let pos = ramps_to_remove.partition_point(|(_, r)| r.lut_start < ramp.lut_start);
            ramp.lut_start -= prefix_sum[pos];
        }
    }
}

/// Used to reinterpret the lifetimes of a vector.
// For how this works, see:
// https://davidlattimore.github.io/posts/2025/09/02/rustforge-wild-performance-tricks.html
fn reuse_vec<T, U>(mut v: Vec<T>) -> Vec<U> {
    const {
        assert!(
            size_of::<T>() == size_of::<U>(),
            "Types must have the same size for safe reinterpretation"
        );
        assert!(
            align_of::<T>() == align_of::<U>(),
            "Types must have the same alignment for safe reinterpretation"
        );
    }
    v.clear();
    v.into_iter().map(|_x| unreachable!()).collect()
}

/// Cached gradient ramp data with metadata.
#[derive(Debug, Clone)]
pub(crate) struct CachedRamp {
    /// Width of this gradient's LUT.
    pub width: u32,
    /// Offset in luts where this ramp starts.
    pub lut_start: u32,
}

/// Generate the gradient LUT.
// TODO: Consider adding a method that generates LUT data directly into output buffer
// to avoid duplicate allocation when lut() is only used once (e.g., in gradient cache).
// The current approach allocates LUT in OnceCell and then copies to output, keeping
// both allocations alive.
#[inline(always)]
fn generate_gradient_lut_impl<S: Simd>(
    simd: S,
    gradient: &EncodedGradient,
    output: &mut Vec<u8>,
) -> usize {
    let lut = gradient.u8_lut(simd);
    let bytes: &[u8] = bytemuck::cast_slice(lut.lut());
    output.reserve(bytes.len());
    output.extend_from_slice(bytes);
    lut.width()
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::vec;
    use vello_common::color::{ColorSpaceTag, DynamicColor, HueDirection};
    use vello_common::encode::{EncodeExt, EncodedPaint};
    use vello_common::kurbo::{Affine, Point};
    use vello_common::peniko::{Color, ColorStop, ColorStops, Gradient, LinearGradientPosition};

    fn insert_entries(cache: &mut GradientRampCache, count: usize) {
        for i in 0..count {
            let offset = i as f32 / count as f32;

            let gradient: Gradient = create_gradient(offset);
            insert_entry(cache, gradient);
        }
    }

    fn insert_entry(cache: &mut GradientRampCache, gradient: Gradient) {
        let encoded_gradient = create_encoded_gradient(gradient);
        cache.get_or_create_ramp(&encoded_gradient);
    }

    fn create_encoded_gradient(gradient: Gradient) -> EncodedGradient {
        let mut encoded_paints = vec![];
        gradient.encode_into(&mut encoded_paints, Affine::IDENTITY, None);
        match encoded_paints.into_iter().last().unwrap() {
            EncodedPaint::Gradient(encoded_gradient) => encoded_gradient,
            _ => panic!("Expected a gradient paint"),
        }
    }

    fn create_gradient(offset: f32) -> Gradient {
        Gradient {
            kind: LinearGradientPosition {
                start: Point::new(0.0, 0.0),
                end: Point::new(100.0, 0.0),
            }
            .into(),
            stops: ColorStops(
                vec![
                    ColorStop {
                        offset: 0.0,
                        color: DynamicColor::from_alpha_color(Color::from_rgb8(255, 0, 0)),
                    },
                    ColorStop {
                        offset,
                        color: DynamicColor::from_alpha_color(Color::from_rgb8(0, 255, 0)),
                    },
                    ColorStop {
                        offset: 1.0,
                        color: DynamicColor::from_alpha_color(Color::from_rgb8(0, 0, 255)),
                    },
                ]
                .into(),
            ),
            interpolation_cs: ColorSpaceTag::Srgb,
            hue_direction: HueDirection::Shorter,
            ..Default::default()
        }
    }

    #[test]
    fn test_cache_empty() {
        let mut cache = GradientRampCache::new(5, Level::baseline());
        cache.maintain();

        assert_eq!(cache.cache.len(), 0);
        assert_eq!(cache.epoch, 0);
        assert!(cache.is_empty());
        assert!(!cache.has_changed());
    }

    #[test]
    fn test_unique_entry_creation() {
        let mut cache = GradientRampCache::new(5, Level::baseline());
        insert_entries(&mut cache, 4);
        cache.maintain();

        assert_eq!(cache.cache.len(), 4);
        assert!(!cache.is_empty());
        assert!(cache.has_changed());
    }

    #[test]
    fn test_no_eviction_under_limit() {
        let mut cache = GradientRampCache::new(5, Level::baseline());
        insert_entries(&mut cache, 4);
        cache.maintain();

        assert_eq!(cache.cache.len(), 4);
    }

    #[test]
    fn test_no_eviction_at_limit() {
        let mut cache = GradientRampCache::new(5, Level::baseline());
        insert_entries(&mut cache, 5);
        cache.maintain();

        assert_eq!(cache.cache.len(), 5);
    }

    #[test]
    fn test_eviction_over_limit() {
        let mut cache = GradientRampCache::new(5, Level::baseline());
        insert_entries(&mut cache, 10);
        cache.maintain();

        // Should trigger eviction since we're over the limit
        assert_eq!(cache.cache.len(), 5);
    }

    #[test]
    fn test_lut_compaction_and_offset_updates() {
        let mut cache = GradientRampCache::new(2, Level::baseline());

        // Start from 1 to keep LUT sizes consistent, making it easier to test LUT size
        // before and after eviction.
        for i in 1..3 {
            let gradient = create_gradient(i as f32 / 10.0);
            insert_entry(&mut cache, gradient);
        }
        let initial_luts_size = cache.luts_size();

        // This should evict the LRU entry and compact the luts
        for i in 3..5 {
            let gradient = create_gradient(i as f32 / 10.0);
            insert_entry(&mut cache, gradient);
        }
        cache.maintain();
        assert_eq!(cache.cache.len(), 2);

        // Verify that luts vector was compacted properly
        assert_eq!(cache.luts_size(), initial_luts_size);

        // Verify that remaining entries have valid, updated offsets
        let mut offsets = Vec::new();
        for (_, (ramp, _)) in &cache.cache {
            offsets.push((ramp.lut_start, ramp.width));
        }
        offsets.sort();

        // The first entry should start at 0 after compaction
        assert_eq!(offsets[0].0, 0);
        // Each subsequent entry should start where the previous one ended
        for i in 1..offsets.len() {
            let expected_start = offsets[i - 1].0 + offsets[i - 1].1;
            assert_eq!(offsets[i].0, expected_start);
        }

        // Total luts size should match sum of all widths * BYTES_PER_TEXEL
        let total_width: u32 = offsets.iter().map(|(_, width)| width).sum();
        assert_eq!(cache.luts.len(), (total_width * BYTES_PER_TEXEL) as usize);
    }

    #[test]
    fn test_correct_lru_eviction() {
        let mut cache = GradientRampCache::new(3, Level::baseline());

        // Insert 3 gradients to fill the cache
        let gradient1 = create_gradient(0.1);
        let gradient2 = create_gradient(0.2);
        let gradient3 = create_gradient(0.3);

        insert_entry(&mut cache, gradient1.clone());
        insert_entry(&mut cache, gradient2.clone());
        insert_entry(&mut cache, gradient3.clone());

        assert_eq!(cache.cache.len(), 3);

        // Access gradient1 and gradient3 to make them more recently used
        // This should make gradient2 the least recently used
        insert_entry(&mut cache, gradient1.clone());
        insert_entry(&mut cache, gradient3.clone());

        // Now insert a new gradient that should evict gradient2
        let gradient4 = create_gradient(0.4);
        insert_entry(&mut cache, gradient4.clone());
        cache.maintain();

        // Cache should still have 3 entries
        assert_eq!(cache.cache.len(), 3);

        // Check gradient1 is still cached
        let encoded_gradient1 = create_encoded_gradient(gradient1);
        assert!(
            cache.cache.contains_key(&encoded_gradient1.cache_key),
            "Gradient1 should still be cached"
        );

        // Check gradient2 was evicted
        let encoded_gradient2 = create_encoded_gradient(gradient2);
        assert!(
            !cache.cache.contains_key(&encoded_gradient2.cache_key),
            "Gradient2 should have been evicted"
        );

        // Check gradient3 is still cached
        let encoded_gradient3 = create_encoded_gradient(gradient3);
        assert!(
            cache.cache.contains_key(&encoded_gradient3.cache_key),
            "Gradient3 should still be cached"
        );

        // Check gradient4 is cached
        let encoded_gradient4 = create_encoded_gradient(gradient4);
        assert!(
            cache.cache.contains_key(&encoded_gradient4.cache_key),
            "Gradient4 should be cached"
        );
    }

    #[test]
    fn test_take_and_restore_luts() {
        let mut cache = GradientRampCache::new(5, Level::baseline());

        let gradient1 = create_gradient(0.1);
        let gradient2 = create_gradient(0.2);
        let gradient3 = create_gradient(0.3);
        insert_entry(&mut cache, gradient1.clone());
        insert_entry(&mut cache, gradient2.clone());
        insert_entry(&mut cache, gradient3.clone());
        cache.maintain();
        let original_size = cache.luts_size();
        let original_cache_size = cache.cache.len();

        let luts = cache.take_luts();
        assert_eq!(luts.len(), original_size);
        assert_eq!(cache.luts_size(), 0);
        assert!(cache.is_empty());

        cache.restore_luts(luts);

        assert_eq!(cache.luts_size(), original_size);
        assert_eq!(cache.cache.len(), original_cache_size);
        assert!(!cache.is_empty());

        let encoded_gradient1 = create_encoded_gradient(gradient1.clone());
        let encoded_gradient2 = create_encoded_gradient(gradient2.clone());
        let encoded_gradient3 = create_encoded_gradient(gradient3.clone());
        assert!(cache.cache.contains_key(&encoded_gradient1.cache_key));
        assert!(cache.cache.contains_key(&encoded_gradient2.cache_key));
        assert!(cache.cache.contains_key(&encoded_gradient3.cache_key));
    }

    #[test]
    fn test_lut_start_invalidation() {
        let mut cache = GradientRampCache::new(2, Level::baseline());

        let gradient_1 = create_encoded_gradient(create_gradient(0.1));
        let gradient_2 = create_encoded_gradient(create_gradient(0.2));
        let gradient_3 = create_encoded_gradient(create_gradient(0.3));

        // Frame 1 start
        let _lut_start_1 = cache.get_or_create_ramp(&gradient_1).0;
        let _lut_start_2 = cache.get_or_create_ramp(&gradient_2).0;
        // Frame 1 end

        // Frame 2 start
        let lut_start_2 = cache.get_or_create_ramp(&gradient_2).0;
        let lut_start_3 = cache.get_or_create_ramp(&gradient_3).0;
        // Frame 2 end

        // Gradient lut shouldn't mutate within a frame
        assert_eq!(
            lut_start_2,
            cache.cache.get(&gradient_2.cache_key).unwrap().0.lut_start
        );
        assert_eq!(
            lut_start_3,
            cache.cache.get(&gradient_3.cache_key).unwrap().0.lut_start
        );
    }
}
