// Pipeline cache — hash-keyed lazy compilation with dedup.
// Avoids re-creating identical render/compute pipelines and shader modules.
// Uses FNV-1a hashing consistent with dreamwell-engine hash infrastructure.

use std::collections::HashMap;

/// Cache statistics for diagnostics.
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
    pub render_pipelines: usize,
    pub compute_pipelines: usize,
    pub shader_modules: usize,
    pub render_hits: u64,
    pub compute_hits: u64,
    pub shader_hits: u64,
}

/// Hash-keyed pipeline compilation cache.
///
/// Stores render pipelines, compute pipelines, and shader modules keyed by
/// FNV-1a hash of their configuration. Closure-based creation avoids owned
/// descriptor lifetime problems.
pub struct PipelineCache {
    render_pipelines: HashMap<u64, wgpu::RenderPipeline>,
    compute_pipelines: HashMap<u64, wgpu::ComputePipeline>,
    shader_modules: HashMap<u64, wgpu::ShaderModule>,
    stats: CacheStats,
}

impl PipelineCache {
    pub fn new() -> Self {
        Self {
            render_pipelines: HashMap::new(),
            compute_pipelines: HashMap::new(),
            shader_modules: HashMap::new(),
            stats: CacheStats::default(),
        }
    }

    /// Get or create a render pipeline. The `create` closure is only called on cache miss.
    pub fn get_or_create_render(
        &mut self,
        device: &wgpu::Device,
        key: u64,
        create: impl FnOnce(&wgpu::Device) -> wgpu::RenderPipeline,
    ) -> &wgpu::RenderPipeline {
        if self.render_pipelines.contains_key(&key) {
            self.stats.render_hits += 1;
        }
        self.render_pipelines.entry(key).or_insert_with(|| {
            self.stats.render_pipelines += 1;
            create(device)
        })
    }

    /// Get or create a compute pipeline. The `create` closure is only called on cache miss.
    pub fn get_or_create_compute(
        &mut self,
        device: &wgpu::Device,
        key: u64,
        create: impl FnOnce(&wgpu::Device) -> wgpu::ComputePipeline,
    ) -> &wgpu::ComputePipeline {
        if self.compute_pipelines.contains_key(&key) {
            self.stats.compute_hits += 1;
        }
        self.compute_pipelines.entry(key).or_insert_with(|| {
            self.stats.compute_pipelines += 1;
            create(device)
        })
    }

    /// Get or create a shader module from WGSL source.
    pub fn get_or_create_shader(&mut self, device: &wgpu::Device, key: u64, source: &str) -> &wgpu::ShaderModule {
        if self.shader_modules.contains_key(&key) {
            self.stats.shader_hits += 1;
        }
        self.shader_modules.entry(key).or_insert_with(|| {
            self.stats.shader_modules += 1;
            device.create_shader_module(wgpu::ShaderModuleDescriptor {
                label: None,
                source: wgpu::ShaderSource::Wgsl(source.into()),
            })
        })
    }

    /// Check if a render pipeline exists for the given key.
    pub fn has_render(&self, key: u64) -> bool {
        self.render_pipelines.contains_key(&key)
    }

    /// Check if a compute pipeline exists for the given key.
    pub fn has_compute(&self, key: u64) -> bool {
        self.compute_pipelines.contains_key(&key)
    }

    /// Cache statistics.
    pub fn stats(&self) -> &CacheStats {
        &self.stats
    }

    /// Total number of cached objects.
    pub fn total_cached(&self) -> usize {
        self.render_pipelines.len() + self.compute_pipelines.len() + self.shader_modules.len()
    }

    /// Clear all cached pipelines and modules.
    pub fn clear(&mut self) {
        self.render_pipelines.clear();
        self.compute_pipelines.clear();
        self.shader_modules.clear();
        self.stats = CacheStats::default();
    }
}

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

/// Compute FNV-1a hash of a byte slice. Consistent with dreamwell-engine hash.
pub fn fnv1a_hash(bytes: &[u8]) -> u64 {
    const FNV_OFFSET: u64 = 14695981039346656037;
    const FNV_PRIME: u64 = 1099511628211;
    let mut hash = FNV_OFFSET;
    for &b in bytes {
        hash ^= b as u64;
        hash = hash.wrapping_mul(FNV_PRIME);
    }
    hash
}

/// Compute a pipeline key from shader source + variant discriminant.
pub fn pipeline_key(shader_source: &str, variant: u32) -> u64 {
    let mut hash = fnv1a_hash(shader_source.as_bytes());
    hash ^= variant as u64;
    hash = hash.wrapping_mul(1099511628211);
    hash
}

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

    #[test]
    fn fnv1a_deterministic() {
        let a = fnv1a_hash(b"hello");
        let b = fnv1a_hash(b"hello");
        assert_eq!(a, b);
    }

    #[test]
    fn fnv1a_different_inputs() {
        let a = fnv1a_hash(b"hello");
        let b = fnv1a_hash(b"world");
        assert_ne!(a, b);
    }

    #[test]
    fn pipeline_key_varies_with_variant() {
        let a = pipeline_key("shader code", 0);
        let b = pipeline_key("shader code", 1);
        assert_ne!(a, b);
    }

    #[test]
    fn pipeline_key_varies_with_source() {
        let a = pipeline_key("shader A", 0);
        let b = pipeline_key("shader B", 0);
        assert_ne!(a, b);
    }

    #[test]
    fn cache_stats_default() {
        let cache = PipelineCache::new();
        assert_eq!(cache.total_cached(), 0);
        assert_eq!(cache.stats().render_pipelines, 0);
        assert_eq!(cache.stats().compute_pipelines, 0);
        assert_eq!(cache.stats().shader_modules, 0);
    }

    #[test]
    fn cache_clear() {
        let mut cache = PipelineCache::new();
        cache.stats.render_hits = 10;
        cache.clear();
        assert_eq!(cache.stats().render_hits, 0);
    }
}
