// Bind group layout cache — prevents duplicate layout creation.
// Keyed by FNV-1a hash of layout entries for dedup across pipeline variants.

use std::collections::HashMap;

use crate::pipeline_cache::fnv1a_hash;

/// Cache for bind group layouts. Prevents creating duplicate layouts
/// when multiple pipelines share the same binding configuration.
pub struct BindGroupLayoutCache {
    layouts: HashMap<u64, wgpu::BindGroupLayout>,
}

impl BindGroupLayoutCache {
    pub fn new() -> Self {
        Self {
            layouts: HashMap::new(),
        }
    }

    /// Get or create a bind group layout. Keyed by caller-provided hash.
    pub fn get_or_create(
        &mut self,
        device: &wgpu::Device,
        key: u64,
        entries: &[wgpu::BindGroupLayoutEntry],
    ) -> &wgpu::BindGroupLayout {
        self.layouts.entry(key).or_insert_with(|| {
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, entries })
        })
    }

    /// Check if a layout exists for the given key.
    pub fn has(&self, key: u64) -> bool {
        self.layouts.contains_key(&key)
    }

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

    /// Whether the cache is empty.
    pub fn is_empty(&self) -> bool {
        self.layouts.is_empty()
    }

    /// Clear all cached layouts.
    pub fn clear(&mut self) {
        self.layouts.clear();
    }
}

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

/// Compute a layout key from bind group layout entries.
/// Hashes binding index, visibility, and type discriminant.
pub fn layout_key(entries: &[wgpu::BindGroupLayoutEntry]) -> u64 {
    let mut bytes = Vec::with_capacity(entries.len() * 8);
    for entry in entries {
        bytes.extend_from_slice(&entry.binding.to_le_bytes());
        bytes.extend_from_slice(&entry.visibility.bits().to_le_bytes());
        // Discriminant of binding type
        let ty_disc: u8 = match entry.ty {
            wgpu::BindingType::Buffer { .. } => 0,
            wgpu::BindingType::Sampler(_) => 1,
            wgpu::BindingType::Texture { .. } => 2,
            wgpu::BindingType::StorageTexture { .. } => 3,
            wgpu::BindingType::AccelerationStructure { .. } => 4,
            _ => 5,
        };
        bytes.push(ty_disc);
    }
    fnv1a_hash(&bytes)
}

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

    #[test]
    fn layout_key_deterministic() {
        let entries = [wgpu::BindGroupLayoutEntry {
            binding: 0,
            visibility: wgpu::ShaderStages::VERTEX,
            ty: wgpu::BindingType::Buffer {
                ty: wgpu::BufferBindingType::Uniform,
                has_dynamic_offset: false,
                min_binding_size: None,
            },
            count: None,
        }];
        let a = layout_key(&entries);
        let b = layout_key(&entries);
        assert_eq!(a, b);
    }

    #[test]
    fn layout_key_different_for_different_bindings() {
        let a_entries = [wgpu::BindGroupLayoutEntry {
            binding: 0,
            visibility: wgpu::ShaderStages::VERTEX,
            ty: wgpu::BindingType::Buffer {
                ty: wgpu::BufferBindingType::Uniform,
                has_dynamic_offset: false,
                min_binding_size: None,
            },
            count: None,
        }];
        let b_entries = [wgpu::BindGroupLayoutEntry {
            binding: 1,
            visibility: wgpu::ShaderStages::FRAGMENT,
            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
            count: None,
        }];
        assert_ne!(layout_key(&a_entries), layout_key(&b_entries));
    }

    #[test]
    fn cache_empty_by_default() {
        let cache = BindGroupLayoutCache::new();
        assert!(cache.is_empty());
        assert_eq!(cache.len(), 0);
    }
}
