//! Material store -- manages material descriptors via `MaterialHandle`.

use crate::handles::MaterialHandle;
use dreamwell_engine::material::LitMaterialDesc;

/// CPU-side material store. GPU upload is deferred to the render pass.
pub struct MaterialStore {
    materials: Vec<Option<LitMaterialDesc>>,
    /// Generation counter per slot for dangling reference detection.
    generations: Vec<u32>,
    next_id: u32,
}

impl MaterialStore {
    /// Create an empty material store.
    pub fn new() -> Self {
        Self {
            materials: Vec::new(),
            generations: Vec::new(),
            next_id: 0,
        }
    }

    /// Insert a material descriptor and return its handle.
    pub fn create_lit(&mut self, desc: LitMaterialDesc) -> MaterialHandle {
        let idx = self.next_id;
        self.next_id += 1;

        if (idx as usize) < self.materials.len() {
            self.materials[idx as usize] = Some(desc);
        } else {
            while self.materials.len() < idx as usize {
                self.materials.push(None);
                self.generations.push(0);
            }
            self.materials.push(Some(desc));
            self.generations.push(0);
        }
        let gen = self.generations[idx as usize];
        MaterialHandle(idx, gen)
    }

    /// Update an existing material.
    pub fn update_lit(&mut self, handle: MaterialHandle, desc: LitMaterialDesc) {
        let idx = handle.0 as usize;
        if idx < self.materials.len() {
            self.materials[idx] = Some(desc);
        }
    }

    /// Get a material descriptor by handle. Returns None if generation mismatches.
    pub fn get(&self, handle: MaterialHandle) -> Option<&LitMaterialDesc> {
        let idx = handle.0 as usize;
        if self.generations.get(idx).copied() != Some(handle.1) {
            return None;
        }
        self.materials.get(idx).and_then(|s| s.as_ref())
    }

    /// Remove a material. Increments generation for dangling reference detection.
    pub fn remove(&mut self, handle: MaterialHandle) -> bool {
        let idx = handle.0 as usize;
        if idx < self.materials.len() && self.materials[idx].is_some() {
            self.materials[idx] = None;
            self.generations[idx] += 1;
            true
        } else {
            false
        }
    }

    /// Check if a handle is valid (index and generation match).
    pub fn contains(&self, handle: MaterialHandle) -> bool {
        let idx = handle.0 as usize;
        self.generations.get(idx).copied() == Some(handle.1) && self.materials.get(idx).is_some_and(|s| s.is_some())
    }

    /// Number of loaded materials.
    pub fn len(&self) -> usize {
        self.materials.iter().filter(|s| s.is_some()).count()
    }

    /// Whether the store is empty.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

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

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

    #[test]
    fn create_and_get() {
        let mut store = MaterialStore::new();
        let h = store.create_lit(LitMaterialDesc::default());
        assert!(store.contains(h));
        let mat = store.get(h).unwrap();
        assert_eq!(mat.roughness, 0.5);
    }

    #[test]
    fn update_lit() {
        let mut store = MaterialStore::new();
        let h = store.create_lit(LitMaterialDesc::default());
        store.update_lit(
            h,
            LitMaterialDesc {
                roughness: 0.9,
                alpha_mode: AlphaMode::Blend,
                ..Default::default()
            },
        );
        assert_eq!(store.get(h).unwrap().roughness, 0.9);
    }

    #[test]
    fn remove() {
        let mut store = MaterialStore::new();
        let h = store.create_lit(LitMaterialDesc::default());
        assert!(store.remove(h));
        assert!(!store.contains(h));
        assert!(store.is_empty());
    }
}
