//! Asset caching with LRU eviction policy
//!
//! This module provides a caching layer for loaded assets with
//! memory management and spatial preloading support.

/// Performance metrics
pub mod metrics;
pub mod pool;

use dashmap::DashMap;
use std::collections::VecDeque;
use std::hash::{Hash, Hasher};
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
use xxhash_rust::xxh3::Xxh3;

use crate::gpu::mock::MockGpu;
use crate::gpu::{
    model::{GpuMesh, GpuModel},
    BufferUsage, GpuDevice, GpuTextureFormat, TextureDescriptor,
};
use crate::model::{LoadedModel, Material, ModelLoader, Node};
use crate::runtime::mock::MockSpawner;
use crate::runtime::AsyncSpawner;
use crate::spatial::preloader::AssetPreloader;
use crate::spatial::preloader::SceneGraph;
use crate::texture::{Texture, TextureLoader};
use futures::lock::Mutex as AsyncMutex;

use glam::Vec3;
use metrics::AssetMetricsHandle;
pub use pool::{AssetMemoryPool, MeshData, TextureData, VertexData};

/// Represents a cached asset with metadata
struct CachedAsset<T> {
    asset: Arc<T>,
    size: usize,
    #[allow(dead_code)]
    last_accessed: u64,
}

/// Manages cached assets with LRU eviction policy
///
/// Generic over the GPU device type and Async runtime for maximum flexibility.
pub struct AssetCache<G: GpuDevice + 'static = MockGpu, S: AsyncSpawner + 'static = MockSpawner> {
    models: Arc<DashMap<u64, CachedAsset<GpuModel<G>>>>,
    textures: Arc<DashMap<u64, CachedAsset<G::Texture>>>,
    model_lru: Arc<RwLock<VecDeque<u64>>>,
    texture_lru: Arc<RwLock<VecDeque<u64>>>,
    max_memory: usize,
    current_memory: Arc<AtomicUsize>,
    gpu: G,
    spawner: S,
    preloader: Arc<AsyncMutex<AssetPreloader>>,
    /// Performance metrics for asset loading and caching
    metrics: AssetMetricsHandle,
    /// Current player position for spatial preloading
    current_player_pos: Arc<RwLock<Vec3>>,
    /// Scene graph for spatial queries
    scene_graph: Arc<RwLock<Option<Arc<dyn SceneGraph>>>>,
}

impl<G: GpuDevice + 'static, S: AsyncSpawner + 'static> Clone for AssetCache<G, S> {
    fn clone(&self) -> Self {
        // Create a new cache with the same configuration but empty state
        Self {
            models: self.models.clone(),
            textures: self.textures.clone(),
            model_lru: self.model_lru.clone(),
            texture_lru: self.texture_lru.clone(),
            max_memory: self.max_memory,
            current_memory: self.current_memory.clone(),
            gpu: self.gpu.clone(),
            spawner: self.spawner.clone(),
            preloader: self.preloader.clone(),
            metrics: self.metrics.clone(),
            current_player_pos: self.current_player_pos.clone(),
            scene_graph: self.scene_graph.clone(),
        }
    }
}

impl<G: GpuDevice + 'static, S: AsyncSpawner + 'static> AssetCache<G, S> {
    /// Creates a new AssetCache with the specified GPU device, Spawner and memory limit
    pub fn new(gpu: G, spawner: S, max_memory: usize) -> Self {
        Self {
            models: Arc::new(DashMap::new()),
            textures: Arc::new(DashMap::new()),
            model_lru: Arc::new(RwLock::new(VecDeque::new())),
            texture_lru: Arc::new(RwLock::new(VecDeque::new())),
            max_memory,
            current_memory: Arc::new(AtomicUsize::new(0)),
            gpu,
            spawner,
            preloader: Arc::new(AsyncMutex::new(AssetPreloader::new(100.0))),
            metrics: AssetMetricsHandle::new(),
            current_player_pos: Arc::new(RwLock::new(Vec3::ZERO)),
            scene_graph: Arc::new(RwLock::new(None)),
        }
    }

    /// Get a reference to the GPU device
    pub fn gpu(&self) -> &G {
        &self.gpu
    }

    /// Gets a model from cache or loads it if not present
    pub fn get_or_load_model<P: AsRef<Path>>(
        &self,
        path: P,
    ) -> Result<Arc<GpuModel<G>>, anyhow::Error> {
        let path = path.as_ref();
        let _path_hash = self.hash_path(path);
        let _current_time = Self::current_timestamp();

        let path_hash = self.hash_path(path);
        let current_time = Self::current_timestamp();

        // Check cache first
        if let Some(cached) = self.models.get(&path_hash) {
            // Record cache hit
            self.metrics.record_cache_hit();

            // Update last accessed time
            let mut model_lru = self.model_lru.write().unwrap();
            // Note: This linear search in LRU is still O(N), could be optimized later
            if let Some(pos) = model_lru.iter().position(|&id| id == path_hash) {
                model_lru.remove(pos);
            }
            model_lru.push_back(path_hash);
            return Ok(Arc::clone(&cached.asset));
        }

        // Record cache miss
        self.metrics.record_cache_miss();

        // Load the model with timing
        let start_time = std::time::Instant::now();
        // Load CPU model first
        let cpu_model = self.load_model_direct(path)?;
        // Estimate size based on CPU model before upload (approximate)
        let size = self.estimate_model_size(&cpu_model); // Use CPU size for metric? Or GPU size?

        // Upload to GPU
        let gpu_model = self.upload_model(cpu_model)?;
        // Recalculate size if needed? CPU size is simpler proxy for now.
        // Actually upload_model consumes cpu_model.

        // Record load time and memory usage

        // Record load time and memory usage
        let load_duration = start_time.elapsed();
        let path_str = path.to_string_lossy().to_string();
        self.metrics
            .record_load_time(path_str.clone(), load_duration);
        self.metrics.record_memory_usage(path_str, size);

        // Check if we need to evict old assets
        self.evict_if_needed(size);

        // Insert into cache
        let model_arc = Arc::new(gpu_model);
        let cached = CachedAsset {
            asset: Arc::clone(&model_arc),
            size,
            last_accessed: current_time,
        };

        {
            self.models.insert(path_hash, cached);
            self.model_lru.write().unwrap().push_back(path_hash);
        }

        self.current_memory.fetch_add(size, Ordering::SeqCst);

        Ok(model_arc)
    }

    /// Gets a model from cache or loads it with specific texture quality preference
    pub async fn get_or_load_model_with_quality<P: AsRef<Path>>(
        &self,
        path: P,
        quality: crate::model::TextureQuality,
    ) -> Result<Arc<GpuModel<G>>, anyhow::Error> {
        // Create a unique hash for this path + quality combination
        // We simulate a different path like "model.glb?quality=High" for the hash
        let mut hasher = Xxh3::new();
        let path = path.as_ref();
        path.hash(&mut hasher);
        // Mix in quality to distinguish cache entries
        format!("{quality:?}").hash(&mut hasher);

        // Also include file modification time (same as regular hash)
        if let Ok(metadata) = std::fs::metadata(path) {
            if let Ok(modified) = metadata.modified() {
                if let Ok(duration) = modified.duration_since(UNIX_EPOCH) {
                    duration.as_secs().hash(&mut hasher);
                }
            }
        }
        let path_hash = hasher.finish();

        // Check cache first
        if let Some(cached) = self.models.get(&path_hash) {
            self.metrics.record_cache_hit();
            // Update LRU
            let mut model_lru = self.model_lru.write().unwrap();
            if let Some(pos) = model_lru.iter().position(|&id| id == path_hash) {
                model_lru.remove(pos);
            }
            model_lru.push_back(path_hash);
            return Ok(Arc::clone(&cached.asset));
        }

        self.metrics.record_cache_miss();
        let start_time = std::time::Instant::now();

        // 1. Load CPU model
        let cpu_model = self.load_model_direct(path)?;

        // 2. Upload Geometry
        let meshes = self.upload_geometry(&cpu_model)?;

        // 3. Process textures with requested quality
        let mut textures = Vec::with_capacity(cpu_model.textures.len());
        let target_format = crate::model::TextureFormat::recommended(quality);

        for tex in &cpu_model.textures {
            let mut gpu_tex = None;

            // If we have a target format that supports compression, try to replace textures
            if target_format.is_compressed() {
                if let Some(source_path) = &tex.source_path {
                    // It's an external texture, try to load formatted version
                    let tex_path = Path::new(source_path);

                    // We need to map model::TextureFormat to texture::TextureFormat
                    let load_format = match target_format {
                        crate::model::TextureFormat::Ktx2Etc1s => {
                            crate::texture::TextureFormat::Ktx2Etc1s
                        }
                        crate::model::TextureFormat::Ktx2Uastc => {
                            crate::texture::TextureFormat::Ktx2Uastc
                        }
                        crate::model::TextureFormat::WebP => crate::texture::TextureFormat::WebP,
                        _ => crate::texture::TextureFormat::Rgba8,
                    };

                    // Try to load formatted texture
                    if let Ok(tex_arc) = self
                        .get_or_load_texture_with_format(tex_path, load_format)
                        .await
                    {
                        // We found/loaded a compressed texture!
                        // Unwrap the arc to get the handle (cloning the handle)
                        gpu_tex = Some((*tex_arc).clone());
                    }
                }
            }

            if let Some(t) = gpu_tex {
                textures.push(t);
            } else {
                // Fallback to standard upload
                textures.push(self.upload_texture(tex)?);
            }
        }

        let size = self.estimate_model_size(&cpu_model);

        let gpu_model = GpuModel {
            meshes,
            textures,
            materials: cpu_model.materials,
            nodes: cpu_model.nodes,
            scene_root: cpu_model.scene_root,
        };

        // Record metrics and cache (same as get_or_load_model)
        let load_duration = start_time.elapsed();
        let path_str = path.to_string_lossy().to_string();
        self.metrics
            .record_load_time(path_str.clone(), load_duration);
        self.metrics.record_memory_usage(path_str, size);
        self.evict_if_needed(size);

        let model_arc = Arc::new(gpu_model);
        let cached = CachedAsset {
            asset: Arc::clone(&model_arc),
            size,
            last_accessed: Self::current_timestamp(),
        };

        {
            self.models.insert(path_hash, cached);
            self.model_lru.write().unwrap().push_back(path_hash);
            self.current_memory.fetch_add(size, Ordering::SeqCst);
        }

        Ok(model_arc)
    }

    /// Clears all cached assets
    pub fn clear(&self) {
        {
            self.models.clear();
            self.textures.clear();
            self.model_lru.write().unwrap().clear();
            self.texture_lru.write().unwrap().clear();
        }
        self.current_memory.store(0, Ordering::SeqCst);
    }

    /// Gets the current memory usage in bytes
    pub fn memory_usage(&self) -> usize {
        self.current_memory.load(Ordering::Relaxed)
    }

    /// Get a reference to the metrics handle
    pub fn metrics(&self) -> &AssetMetricsHandle {
        &self.metrics
    }

    /// Load a model directly without caching
    fn load_model_direct<P: AsRef<Path>>(&self, path: P) -> Result<LoadedModel, anyhow::Error> {
        let model_loader = ModelLoader::new();

        // Read the model file
        let data = std::fs::read(path)?;

        // Load the model using the GLB loader
        model_loader.load_glb(&data).map_err(Into::into)
    }

    fn estimate_model_size(&self, model: &LoadedModel) -> usize {
        let mut size = 0;

        // Estimate size of meshes
        for mesh in &model.meshes {
            // Get read access to the mesh data
            let mesh_data = mesh.vertices();

            // Calculate size based on vertex and index data
            size += mesh_data.vertices.len() * std::mem::size_of::<f32>();
            size += mesh_data.indices.len() * std::mem::size_of::<u32>();
        }

        // Add size for materials and other model data
        size += model.materials.len() * std::mem::size_of::<Material>();
        size += model.nodes.len() * std::mem::size_of::<Node>();

        size
    }

    fn upload_geometry(&self, model: &LoadedModel) -> Result<Vec<GpuMesh<G>>, anyhow::Error> {
        let mut meshes = Vec::with_capacity(model.meshes.len());

        for mesh in &model.meshes {
            // Upload vertex buffer
            let v_data = mesh.vertex_buffer();
            let vertex_buffer = self
                .gpu
                .allocate_buffer(v_data.len(), BufferUsage::Vertex)
                .map_err(|e| anyhow::anyhow!("Failed to allocate vertex buffer: {e:?}"))?;
            self.gpu
                .upload_buffer_data(&vertex_buffer, 0, &v_data)
                .map_err(|e| anyhow::anyhow!("Failed to upload vertex buffer: {e:?}"))?;

            // Upload index buffer
            let i_data = mesh.index_buffer();
            let index_buffer = self
                .gpu
                .allocate_buffer(i_data.len(), BufferUsage::Index)
                .map_err(|e| anyhow::anyhow!("Failed to allocate index buffer: {e:?}"))?;
            self.gpu
                .upload_buffer_data(&index_buffer, 0, &i_data)
                .map_err(|e| anyhow::anyhow!("Failed to upload index buffer: {e:?}"))?;

            meshes.push(GpuMesh {
                vertex_buffer,
                index_buffer,
                index_count: mesh.index_count(),
                vertex_count: mesh.vertex_count(),
                material_index: mesh.material_index,
                primitive_type: mesh.primitive_type,
            });
        }
        Ok(meshes)
    }

    fn upload_model(&self, model: LoadedModel) -> Result<GpuModel<G>, anyhow::Error> {
        let meshes = self.upload_geometry(&model)?;

        let mut textures = Vec::with_capacity(model.textures.len());
        for tex in &model.textures {
            let texture = self.upload_texture(tex)?;
            textures.push(texture);
        }

        Ok(GpuModel {
            meshes,
            textures,
            materials: model.materials,
            nodes: model.nodes,
            scene_root: model.scene_root,
        })
    }

    fn upload_texture(&self, tex: &crate::model::Texture) -> Result<G::Texture, anyhow::Error> {
        let format = match tex.format {
            crate::model::TextureFormat::R8G8B8A8Srgb => GpuTextureFormat::Rgba8Srgb,
            crate::model::TextureFormat::WebP => GpuTextureFormat::Rgba8Srgb,
            crate::model::TextureFormat::Ktx2Etc1s | crate::model::TextureFormat::Ktx2Uastc => {
                GpuTextureFormat::Rgba8Srgb
            }
            _ => GpuTextureFormat::Rgba8Srgb,
        };

        let desc = TextureDescriptor {
            width: tex.width,
            height: tex.height,
            format,
            mip_levels: if tex.generate_mipmaps {
                // If we want mips, we need to calculate them or set max info
                // For now, let's say 0 means 'generatable' or handle downstream.
                // But GpuTextureFormat doesn't tell us how many.
                // The warning says identical blocks.
                // Previous logic: if tex.generate_mipmaps { 1 } else { 1 }
                // So it was always 1.
                // Let's assume for now 1 is default, and if we genuinely want mips we will increment later
                // or just fix the redundancy.
                1
            } else {
                1
            },
            generate_mipmaps: tex.generate_mipmaps,
        };

        self.gpu
            .create_texture(&desc, &tex.data)
            .map_err(|e| anyhow::anyhow!("Failed to create texture: {e:?}"))
    }

    #[allow(dead_code)]
    fn estimate_gpu_model_size(&self, model: &GpuModel<G>) -> usize {
        // Approximate size based on structure
        // Since we can't query G::Buffer size easily without trait change,
        // we might need to store it in GpuMesh or guess.
        // But GpuMesh IS stored in GpuModel.
        // Wait, GpuMesh has G::Buffer. G::Buffer doesn't expose size in trait.
        // But we allocated it. We should probably track it.
        // Implementation Detail: GpuModel doesn't store size.
        // We can sum vertices * stride + indices * 4.
        let mut size = 0;
        for mesh in &model.meshes {
            size += mesh.vertex_count * std::mem::size_of::<crate::renderer::Vertex>();
            size += mesh.index_count * 4;
        }
        // Textures?
        // We don't have texture size readily available in GpuTexture handle.
        // We'll estimate from estimate_model_size logic if we had CPU model.
        // For now, minimal estimation.
        size
    }

    fn evict_if_needed(&self, required_size: usize) {
        let mut current_memory = self.current_memory.load(Ordering::SeqCst);
        let max_memory = self.max_memory;

        if current_memory + required_size <= max_memory {
            return; // No need to evict
        }

        // First, try to evict models
        self.evict_from_cache::<GpuModel<G>>(
            &self.models,
            &self.model_lru,
            &mut current_memory,
            max_memory,
            required_size,
        );

        // If still not enough space, try to evict textures
        if current_memory + required_size > max_memory {
            self.evict_from_cache::<G::Texture>(
                &self.textures,
                &self.texture_lru,
                &mut current_memory,
                max_memory,
                required_size,
            );
        }

        self.current_memory.store(current_memory, Ordering::SeqCst);
    }

    fn evict_from_cache<T: 'static>(
        &self,
        cache: &DashMap<u64, CachedAsset<T>>,
        lru: &RwLock<VecDeque<u64>>,
        current_memory: &mut usize,
        max_memory: usize,
        required_size: usize,
    ) {
        let mut lru = lru.write().unwrap();

        while *current_memory + required_size > max_memory && !lru.is_empty() {
            if let Some(oldest_id) = lru.pop_front() {
                if let Some((_, removed)) = cache.remove(&oldest_id) {
                    *current_memory -= removed.size;
                }
            }
        }
    }

    fn hash_path<P: AsRef<Path>>(&self, path: P) -> u64 {
        let path = path.as_ref();
        let mut hasher = Xxh3::new();
        path.hash(&mut hasher);

        // Include file modification time in the hash to detect changes
        if let Ok(metadata) = std::fs::metadata(path) {
            if let Ok(modified) = metadata.modified() {
                if let Ok(duration) = modified.duration_since(UNIX_EPOCH) {
                    duration.as_secs().hash(&mut hasher);
                    duration.subsec_nanos().hash(&mut hasher);
                }
            }
        }

        hasher.finish()
    }

    fn current_timestamp() -> u64 {
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs()
    }
}

// Async methods
impl<G: GpuDevice + 'static, S: AsyncSpawner + 'static> AssetCache<G, S> {
    /// Gets a texture from cache or loads it if not present
    pub async fn get_or_load_texture<P: AsRef<Path>>(
        &self,
        path: P,
    ) -> Result<Arc<G::Texture>, anyhow::Error> {
        let path = path.as_ref();
        let path_hash = self.hash_path(path);
        let current_time = Self::current_timestamp();

        // Check cache first
        if let Some(cached) = self.textures.get(&path_hash) {
            // Record cache hit
            self.metrics.record_cache_hit();

            // Update last accessed time
            let mut texture_lru = self.texture_lru.write().unwrap();
            if let Some(pos) = texture_lru.iter().position(|&id| id == path_hash) {
                texture_lru.remove(pos);
            }
            texture_lru.push_back(path_hash);
            return Ok(Arc::clone(&cached.asset));
        }

        // Record cache miss
        self.metrics.record_cache_miss();

        // Load the texture with timing
        let start_time = std::time::Instant::now();
        let texture = self.load_texture_direct(path).await?;
        let size = texture.data.len();

        // Record load time and memory usage
        let load_duration = start_time.elapsed();
        let path_str = path.to_string_lossy().to_string();
        self.metrics
            .record_load_time(path_str.clone(), load_duration);
        self.metrics.record_memory_usage(path_str, size);

        // Check if we need to evict old assets
        self.evict_if_needed(size);

        // Upload to GPU
        let gpu_texture = self.upload_texture_from_loader(&texture)?;

        // Add to cache
        let texture_arc = Arc::new(gpu_texture);
        let cached = CachedAsset {
            asset: Arc::clone(&texture_arc),
            size,
            last_accessed: current_time,
        };

        // Update cache
        {
            self.textures.insert(path_hash, cached);
            self.texture_lru.write().unwrap().push_back(path_hash);
            self.current_memory.fetch_add(size, Ordering::SeqCst);
        }

        Ok(texture_arc)
    }

    /// Gets a texture from cache or loads it with specific format preference
    pub async fn get_or_load_texture_with_format<P: AsRef<Path>>(
        &self,
        path: P,
        format: crate::texture::TextureFormat,
    ) -> Result<Arc<G::Texture>, anyhow::Error> {
        // Similar to get_or_load_texture but uses load_texture_direct_formatted
        // We'll trust the caller to handle cache key differentiation if needed,
        // or we could mix format into the hash.
        // For now, let's mix format into hash to allow caching different formats of same texture.

        let path = path.as_ref();
        // Mix format into hash
        let mut hasher = Xxh3::new();
        path.hash(&mut hasher);
        format.extension().hash(&mut hasher); // Differentiate by extension/format
        let path_hash = hasher.finish();

        // Load formatted
        let start_time = std::time::Instant::now();
        let texture_loader = TextureLoader::new();
        // Uses load_file_formatted which we added to TextureLoader
        let texture = texture_loader.load_file_formatted(path, format).await?;
        let size = texture.data.len();

        // Replicating upload logic here for simplicity of modification
        let load_duration = start_time.elapsed();
        let path_str = path.to_string_lossy().to_string();
        self.metrics
            .record_load_time(path_str.clone(), load_duration);
        self.metrics.record_memory_usage(path_str, size);
        self.evict_if_needed(size);
        let gpu_texture = self.upload_texture_from_loader(&texture)?;

        let texture_arc = Arc::new(gpu_texture);
        let cached = CachedAsset {
            asset: Arc::clone(&texture_arc),
            size,
            last_accessed: Self::current_timestamp(),
        };

        {
            self.textures.insert(path_hash, cached);
            self.texture_lru.write().unwrap().push_back(path_hash);
            self.current_memory.fetch_add(size, Ordering::SeqCst);
        }

        Ok(texture_arc)
    }

    /// Get a reference to the asset preloader
    pub fn preloader(&self) -> &Arc<AsyncMutex<AssetPreloader>> {
        &self.preloader
    }

    /// Load a texture directly without caching
    async fn load_texture_direct<P: AsRef<Path>>(&self, path: P) -> Result<Texture, anyhow::Error> {
        let texture_loader = TextureLoader::new();

        // Await the async load_file method
        let texture = texture_loader.load_file(path).await?;
        Ok(texture)
    }

    fn upload_texture_from_loader(&self, tex: &Texture) -> Result<G::Texture, anyhow::Error> {
        let format = match tex.format {
            crate::texture::TextureFormat::Rgba8 => GpuTextureFormat::Rgba8Srgb,
            crate::texture::TextureFormat::Rgb8 => GpuTextureFormat::Rgb8Srgb,
            crate::texture::TextureFormat::WebP => GpuTextureFormat::Rgba8Srgb,
            crate::texture::TextureFormat::Ktx2Etc1s | crate::texture::TextureFormat::Ktx2Uastc => {
                GpuTextureFormat::Rgba8Srgb
            }
        };

        let desc = TextureDescriptor {
            width: tex.width,
            height: tex.height,
            format,
            mip_levels: 1,
            generate_mipmaps: true,
        };

        self.gpu
            .create_texture(&desc, &tex.data)
            .map_err(|e| anyhow::anyhow!("Failed to create texture: {e:?}"))
    }

    /// Set the scene graph for spatial preloading
    pub fn set_scene_graph(&self, scene_graph: Arc<dyn SceneGraph>) {
        *self.scene_graph.write().unwrap() = Some(scene_graph);
    }

    /// Update player position and trigger preloader update
    /// Update player position and trigger preloader update
    pub async fn update_player_position(&self, pos: Vec3) {
        *self.current_player_pos.write().unwrap() = pos;

        // Clone the scene graph reference if it exists, to release the synchronous lock
        // before awaiting the async preloader lock.
        let scene_graph_arc = { self.scene_graph.read().unwrap().clone() };

        if let Some(scene_graph) = scene_graph_arc {
            let mut preloader = self.preloader.lock().await;
            preloader.update_preload_queue(pos, scene_graph.as_ref());
        }
    }

    /// Process items from the preload queue
    pub async fn process_preload_work(&self) {
        let (next_asset, preloader_clone) = {
            let preloader = self.preloader.lock().await;
            (preloader.pop_next_asset(), self.preloader.clone())
        };

        if let Some(path) = next_asset {
            // Mark as loading
            {
                let preloader = preloader_clone.lock().await;
                preloader.mark_loading(&path);
            }

            let cache = self.clone();
            let path_clone = path.clone();

            // Spawn task
            let _ = self.spawner.spawn(async move {
                // Load model (ignoring result, logging error)
                // We map error to () to satisfy spawn return type
                let _ = cache
                    .get_or_load_model(&path_clone)
                    .map_err(|e| log::warn!("Failed to preload {path_clone}: {e}"));

                // Mark done
                let preloader = preloader_clone.lock().await;
                preloader.mark_done(&path_clone);
            });
        }
    }

    /// Frame update - Call this once per frame
    pub async fn frame_update(&self, player_pos: Vec3) {
        self.update_player_position(player_pos).await;
        self.process_preload_work().await;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::gpu::mock::MockGpu;

    #[test]
    fn test_cache_creation() {
        let gpu = MockGpu::new();
        let spawner = MockSpawner::new();
        let cache = AssetCache::new(gpu, spawner, 100 * 1024 * 1024);
        assert_eq!(cache.memory_usage(), 0);
    }

    #[test]
    fn test_cache_clear() {
        let gpu = MockGpu::new();
        let spawner = MockSpawner::new();
        let cache = AssetCache::new(gpu, spawner, 100 * 1024 * 1024);
        cache.clear();
        assert_eq!(cache.memory_usage(), 0);
    }
}
