//! Thumbnail cache for asset browser grid cells.

use std::collections::{HashMap, HashSet};

use bevy::prelude::*;
use bevy_egui::{egui, EguiPrimaryContextPass, EguiTextureHandle, EguiUserTextures};
use egui_phosphor_icons::icons;

use super::sources::gltf::{gltf_base_color_texture_path, validate_gltf_dependencies};
use super::studio::{model_file_exists, ThumbnailStudio};
use super::ThumbnailJobSource;
use crate::assets::{
    asset_cache_key, asset_server_path, EditorAsset, EditorAssetKind, EditorAssets,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ThumbnailState {
    Pending,
    Ready,
    Failed { reason: String, retryable: bool },
}

#[derive(Debug, Clone)]
struct ThumbnailFailure {
    reason: String,
    retryable: bool,
}

#[derive(Resource, Default)]
pub struct AssetThumbnailCache {
    pub texture_ids: HashMap<String, egui::TextureId>,
    pending: HashMap<String, Handle<Image>>,
    pub(crate) studio_pending: HashSet<String>,
    failed: HashMap<String, ThumbnailFailure>,
    prefetched_folder: Option<String>,
}

pub struct AssetThumbnailsPlugin;

impl Plugin for AssetThumbnailsPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<AssetThumbnailCache>()
            .add_systems(EguiPrimaryContextPass, register_loaded_thumbnails);
    }
}

impl AssetThumbnailCache {
    pub fn state(&self, key: &str) -> Option<ThumbnailState> {
        if self.texture_ids.contains_key(key) {
            return Some(ThumbnailState::Ready);
        }
        if self.failed.contains_key(key) {
            let failure = self.failed.get(key)?;
            return Some(ThumbnailState::Failed {
                reason: failure.reason.clone(),
                retryable: failure.retryable,
            });
        }
        if self.pending.contains_key(key) || self.studio_pending.contains(key) {
            return Some(ThumbnailState::Pending);
        }
        None
    }

    pub fn retry(&mut self, key: &str) {
        self.failed.remove(key);
        self.pending.remove(key);
        self.studio_pending.remove(key);
        self.texture_ids.remove(key);
    }

    pub fn request_texture(&mut self, key: String, path: String, asset_server: &AssetServer) {
        if matches!(
            self.state(&key),
            Some(ThumbnailState::Ready | ThumbnailState::Pending)
        ) {
            return;
        }
        if self.failed.contains_key(&key) {
            return;
        }
        let handle: Handle<Image> = asset_server.load(asset_server_path(&path));
        self.pending.insert(key, handle);
    }

    pub fn request_model(
        &mut self,
        key: String,
        model_path: String,
        asset_server: &AssetServer,
        studio: &mut ThumbnailStudio,
    ) {
        if matches!(
            self.state(&key),
            Some(ThumbnailState::Ready | ThumbnailState::Pending)
        ) {
            return;
        }
        if self.failed.contains_key(&key) {
            return;
        }

        if !self.model_source_ready(&key, &model_path) {
            return;
        }

        if let Some(texture_path) = gltf_base_color_texture_path(&model_path) {
            self.request_texture(key, texture_path, asset_server);
            return;
        }

        if studio.enqueue(key.clone(), model_path) {
            self.studio_pending.insert(key);
        }
    }

    pub fn request_mesh_subasset(
        &mut self,
        key: String,
        model_path: String,
        mesh_label: String,
        material_label: Option<String>,
        requires_scene_instance: bool,
        studio: &mut ThumbnailStudio,
    ) {
        if matches!(
            self.state(&key),
            Some(ThumbnailState::Ready | ThumbnailState::Pending)
        ) || self.failed.contains_key(&key)
        {
            return;
        }
        if !self.model_source_ready(&key, &model_path) {
            return;
        }
        let source = if requires_scene_instance {
            ThumbnailJobSource::Model { model_path }
        } else {
            ThumbnailJobSource::MeshSubAsset {
                model_path,
                mesh_label,
                material_label,
            }
        };
        if studio.enqueue_source(key.clone(), source) {
            self.studio_pending.insert(key);
        }
    }

    pub fn request_source_material(
        &mut self,
        key: String,
        model_path: String,
        material_label: String,
        studio: &mut ThumbnailStudio,
    ) {
        if matches!(
            self.state(&key),
            Some(ThumbnailState::Ready | ThumbnailState::Pending)
        ) || self.failed.contains_key(&key)
        {
            return;
        }
        if !self.model_source_ready(&key, &model_path) {
            return;
        }
        if studio.enqueue_source(
            key.clone(),
            ThumbnailJobSource::SourceMaterial {
                model_path,
                material_label,
            },
        ) {
            self.studio_pending.insert(key);
        }
    }

    pub fn request_material_asset(
        &mut self,
        key: String,
        path: String,
        studio: &mut ThumbnailStudio,
    ) {
        if matches!(
            self.state(&key),
            Some(ThumbnailState::Ready | ThumbnailState::Pending)
        ) || self.failed.contains_key(&key)
        {
            return;
        }
        let material = match shared::load_resolved_material_from_path(&path) {
            Ok((material, _)) => material,
            Err(error) => {
                self.mark_studio_failed(&key, &error, true);
                return;
            }
        };
        let label = shared::MaterialAsset::load_from_path(&path)
            .map(|asset| asset.label)
            .or_else(|_| {
                shared::MaterialInstanceAsset::load_from_path(&path).map(|asset| asset.label)
            })
            .unwrap_or_else(|_| "Material".into());
        if studio.enqueue_source(
            key.clone(),
            ThumbnailJobSource::MaterialAsset {
                label,
                material: Box::new(material),
            },
        ) {
            self.studio_pending.insert(key);
        }
    }

    fn model_source_ready(&mut self, key: &str, model_path: &str) -> bool {
        if !model_file_exists(model_path) {
            self.mark_studio_failed(key, "file not found", false);
            return false;
        }
        if let Err(reason) = validate_gltf_dependencies(model_path) {
            self.mark_studio_failed(key, &reason, false);
            return false;
        }
        true
    }

    pub(crate) fn complete_studio_thumbnail(
        &mut self,
        key: &str,
        image: Handle<Image>,
        textures: &mut EguiUserTextures,
    ) {
        self.studio_pending.remove(key);
        self.failed.remove(key);
        let texture_id = textures
            .image_id(&image)
            .unwrap_or_else(|| textures.add_image(EguiTextureHandle::Strong(image)));
        self.texture_ids.insert(key.to_string(), texture_id);
    }

    pub(crate) fn mark_studio_failed(&mut self, key: &str, reason: &str, retryable: bool) {
        self.studio_pending.remove(key);
        self.pending.remove(key);
        self.failed.insert(
            key.to_string(),
            ThumbnailFailure {
                reason: reason.to_string(),
                retryable,
            },
        );
    }

    pub fn invalidate_all(&mut self) {
        self.texture_ids.clear();
        self.pending.clear();
        self.studio_pending.clear();
        self.failed.clear();
        self.prefetched_folder = None;
    }

    pub fn snapshot(&self) -> ThumbnailCacheSnapshot {
        ThumbnailCacheSnapshot {
            texture_ids: self.texture_ids.clone(),
            pending_keys: self
                .pending
                .keys()
                .chain(self.studio_pending.iter())
                .cloned()
                .collect(),
            failed_keys: self.failed.keys().cloned().collect(),
        }
    }
}

fn register_loaded_thumbnails(
    asset_server: Res<AssetServer>,
    mut cache: ResMut<AssetThumbnailCache>,
    mut textures: ResMut<EguiUserTextures>,
) {
    let ready: Vec<(String, Handle<Image>)> = cache
        .pending
        .iter()
        .filter(|(_, handle)| asset_server.is_loaded_with_dependencies(*handle))
        .map(|(key, handle)| (key.clone(), handle.clone()))
        .collect();

    for (key, handle) in ready {
        cache.pending.remove(&key);
        cache.failed.remove(&key);
        let texture_id = textures
            .image_id(&handle)
            .unwrap_or_else(|| textures.add_image(EguiTextureHandle::Strong(handle)));
        cache.texture_ids.insert(key, texture_id);
    }
}

pub fn kind_icon(kind: &EditorAssetKind) -> egui_phosphor_icons::Icon {
    match kind {
        EditorAssetKind::Primitive(_) => icons::CUBE,
        EditorAssetKind::Light(_) => icons::LIGHTBULB,
        EditorAssetKind::Model => icons::CUBE_TRANSPARENT,
        EditorAssetKind::Texture => icons::IMAGE,
        EditorAssetKind::Material => icons::PAINT_BRUSH,
        EditorAssetKind::AudioClip => icons::WAVEFORM,
        EditorAssetKind::Level => icons::MAP_TRIFOLD,
        EditorAssetKind::Prefab => icons::PACKAGE,
        EditorAssetKind::PostProcessVolume => icons::CAMERA,
        EditorAssetKind::PostProcessEffect => icons::SPARKLE,
        EditorAssetKind::RenderingProfile => icons::SLIDERS,
        EditorAssetKind::ShaderSchema => icons::CODE,
    }
}

pub fn draw_asset_cell_with(
    ui: &mut egui::Ui,
    asset: &EditorAsset,
    texture_id: Option<egui::TextureId>,
    pending: bool,
    failed: Option<&str>,
    selected: bool,
    thumbnail_size: f32,
) -> egui::Response {
    let thumb_size = thumbnail_size.clamp(48.0, 112.0);
    let cell_size = egui::vec2(thumb_size + 20.0, thumb_size + 30.0);
    let (rect, response) = ui.allocate_exact_size(cell_size, egui::Sense::click_and_drag());

    let fill = if selected {
        crate::ui::theme::SELECTION_BG_MUTED
    } else if response.hovered() {
        crate::ui::theme::ELEVATED_BG
    } else {
        crate::ui::theme::WIDGET_BG
    };
    ui.painter().rect(
        rect,
        4.0,
        fill,
        egui::Stroke::new(
            1.0,
            if selected {
                crate::ui::theme::ACCENT_HOVER
            } else {
                crate::ui::theme::BORDER
            },
        ),
        egui::StrokeKind::Inside,
    );

    let thumb_rect = egui::Rect::from_min_size(
        rect.min + egui::vec2(10.0, 8.0),
        egui::vec2(thumb_size, thumb_size),
    );

    if let Some(texture_id) = texture_id {
        ui.painter().image(
            texture_id,
            thumb_rect,
            egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
            egui::Color32::WHITE,
        );
    } else if pending {
        ui.painter().text(
            thumb_rect.center(),
            egui::Align2::CENTER_CENTER,
            icons::CIRCLE_NOTCH.as_str(),
            egui::FontId::new(24.0, egui::FontFamily::Name("phosphor-regular".into())),
            crate::ui::theme::TEXT_DIM,
        );
    } else if failed.is_some() {
        ui.painter().text(
            thumb_rect.center(),
            egui::Align2::CENTER_CENTER,
            icons::WARNING_CIRCLE.as_str(),
            egui::FontId::new(24.0, egui::FontFamily::Name("phosphor-regular".into())),
            crate::ui::theme::TEXT_DIM,
        );
    } else {
        let icon = kind_icon(&asset.kind);
        ui.painter().text(
            thumb_rect.center(),
            egui::Align2::CENTER_CENTER,
            icon.as_str(),
            egui::FontId::new(28.0, egui::FontFamily::Name("phosphor-regular".into())),
            crate::ui::theme::TEXT,
        );
    }

    ui.painter().text(
        egui::pos2(rect.center().x, rect.max.y - 6.0),
        egui::Align2::CENTER_BOTTOM,
        asset.label.as_str(),
        egui::FontId::new(11.0, egui::FontFamily::Proportional),
        if selected {
            crate::ui::theme::TEXT_SELECTED
        } else {
            crate::ui::theme::TEXT
        },
    );

    response
}

#[derive(Clone)]
pub struct ThumbnailCacheSnapshot {
    pub texture_ids: HashMap<String, egui::TextureId>,
    pub pending_keys: Vec<String>,
    pub failed_keys: Vec<String>,
}

impl ThumbnailCacheSnapshot {
    pub fn texture_for_key(&self, key: &str) -> Option<egui::TextureId> {
        self.texture_ids.get(key).copied()
    }

    pub fn is_pending_key(&self, key: &str) -> bool {
        self.pending_keys.iter().any(|pending| pending == key)
    }

    pub fn is_failed_key(&self, key: &str) -> bool {
        self.failed_keys.iter().any(|failed| failed == key)
    }

    pub fn texture_for(&self, asset: &EditorAsset) -> Option<egui::TextureId> {
        self.texture_ids.get(&asset_cache_key(asset)).copied()
    }

    pub fn is_pending(&self, asset: &EditorAsset) -> bool {
        self.pending_keys
            .iter()
            .any(|key| key == &asset_cache_key(asset))
    }

    pub fn is_failed(&self, asset: &EditorAsset) -> bool {
        self.failed_keys
            .iter()
            .any(|key| key == &asset_cache_key(asset))
    }
}

pub fn prefetch_folder_thumbnails(world: &mut World, folder: &str) {
    {
        let cache = world.resource::<AssetThumbnailCache>();
        if cache.prefetched_folder.as_deref() == Some(folder) {
            return;
        }
    }

    let requests: Vec<(String, String, EditorAssetKind)> = {
        let assets = world.resource::<EditorAssets>();
        assets
            .assets
            .iter()
            .filter(|asset| asset.folder_path == folder)
            .filter(|asset| {
                matches!(
                    asset.kind,
                    EditorAssetKind::Texture | EditorAssetKind::Model | EditorAssetKind::Material
                )
            })
            .filter_map(|asset| {
                Some((
                    asset_cache_key(asset),
                    asset.path.clone()?,
                    asset.kind.clone(),
                ))
            })
            .collect()
    };
    let asset_server = world.resource::<AssetServer>().clone();
    world.resource_scope(|world, mut cache: Mut<AssetThumbnailCache>| {
        world.resource_scope(|_world, mut studio: Mut<ThumbnailStudio>| {
            for (key, path, kind) in requests {
                match kind {
                    EditorAssetKind::Texture => cache.request_texture(key, path, &asset_server),
                    EditorAssetKind::Model => {
                        cache.request_model(key, path, &asset_server, &mut studio)
                    }
                    EditorAssetKind::Material => {
                        cache.request_material_asset(key, path, &mut studio)
                    }
                    _ => {}
                }
            }
        });
    });
    world
        .resource_mut::<AssetThumbnailCache>()
        .prefetched_folder = Some(folder.to_string());
}

/// Requests thumbnails for an explicit cross-folder asset set.
pub fn prefetch_asset_thumbnails(world: &mut World, assets: &[EditorAsset]) {
    let requests = assets
        .iter()
        .filter_map(|asset| {
            Some((
                asset_cache_key(asset),
                asset.path.clone()?,
                asset.kind.clone(),
            ))
        })
        .collect::<Vec<_>>();
    let asset_server = world.resource::<AssetServer>().clone();
    world.resource_scope(|world, mut cache: Mut<AssetThumbnailCache>| {
        world.resource_scope(|_world, mut studio: Mut<ThumbnailStudio>| {
            for (key, path, kind) in requests {
                match kind {
                    EditorAssetKind::Texture => cache.request_texture(key, path, &asset_server),
                    EditorAssetKind::Model => {
                        cache.request_model(key, path, &asset_server, &mut studio)
                    }
                    EditorAssetKind::Material => {
                        cache.request_material_asset(key, path, &mut studio)
                    }
                    _ => {}
                }
            }
        });
    });
}

pub fn invalidate_on_catalog_refresh(world: &mut World) {
    world.resource_mut::<AssetThumbnailCache>().invalidate_all();
    if !world.contains_resource::<ThumbnailStudio>() {
        return;
    }
    let cleanup = {
        let mut studio = world.resource_mut::<ThumbnailStudio>();
        studio.clear_queue();
        studio.clear_jobs()
    };
    if let Some(root) = cleanup.active_root {
        world.commands().entity(root).despawn_children();
        world.commands().entity(root).despawn();
    }
}
