use std::collections::HashMap;
use std::path::{Path, PathBuf};

use super::asset_uuid::AssetUuid;
use super::manifest::{AssetEntry, AssetManifest, AssetType};

#[derive(Debug, Clone, Default)]
pub struct AssetRegistry {
    pub manifests: Vec<AssetManifest>,
    pub uuid_to_manifest: HashMap<AssetUuid, usize>,
    pub path_to_uuid: HashMap<String, AssetUuid>,
    pub name_to_uuid: HashMap<String, AssetUuid>,
    pub base_paths: Vec<PathBuf>,
}

pub fn asset_registry_add_manifest(
    registry: &mut AssetRegistry,
    manifest: AssetManifest,
    base_path: impl Into<PathBuf>,
) {
    let manifest_index = registry.manifests.len();
    let base_path = base_path.into();

    for entry in &manifest.assets {
        registry.uuid_to_manifest.insert(entry.uuid, manifest_index);
        registry.path_to_uuid.insert(entry.path.clone(), entry.uuid);
        if let Some(name) = &entry.name {
            registry.name_to_uuid.insert(name.clone(), entry.uuid);
        }
    }

    registry.manifests.push(manifest);
    registry.base_paths.push(base_path);
}

pub fn asset_registry_resolve_uuid(registry: &AssetRegistry, uuid: AssetUuid) -> Option<PathBuf> {
    let manifest_index = registry.uuid_to_manifest.get(&uuid)?;
    let manifest = registry.manifests.get(*manifest_index)?;
    let entry = manifest.get_by_uuid(uuid)?;
    let base_path = registry.base_paths.get(*manifest_index)?;
    Some(base_path.join(&entry.path))
}

pub fn asset_registry_resolve_path(registry: &AssetRegistry, path: &str) -> Option<AssetUuid> {
    registry.path_to_uuid.get(path).copied()
}

pub fn asset_registry_resolve_name(registry: &AssetRegistry, name: &str) -> Option<AssetUuid> {
    registry.name_to_uuid.get(name).copied()
}

pub fn asset_registry_get_entry(registry: &AssetRegistry, uuid: AssetUuid) -> Option<&AssetEntry> {
    let manifest_index = registry.uuid_to_manifest.get(&uuid)?;
    let manifest = registry.manifests.get(*manifest_index)?;
    manifest.get_by_uuid(uuid)
}

pub fn asset_registry_get_base_path(registry: &AssetRegistry, uuid: AssetUuid) -> Option<&Path> {
    let manifest_index = registry.uuid_to_manifest.get(&uuid)?;
    registry
        .base_paths
        .get(*manifest_index)
        .map(|path| path.as_path())
}

pub fn asset_registry_assets_of_type(
    registry: &AssetRegistry,
    asset_type: AssetType,
) -> impl Iterator<Item = &AssetEntry> {
    registry
        .manifests
        .iter()
        .flat_map(move |manifest| manifest.assets_of_type(asset_type))
}

pub fn asset_registry_all_assets(registry: &AssetRegistry) -> impl Iterator<Item = &AssetEntry> {
    registry
        .manifests
        .iter()
        .flat_map(|manifest| &manifest.assets)
}

pub fn asset_registry_uuid_exists(registry: &AssetRegistry, uuid: AssetUuid) -> bool {
    registry.uuid_to_manifest.contains_key(&uuid)
}

pub fn asset_registry_path_exists(registry: &AssetRegistry, path: &str) -> bool {
    registry.path_to_uuid.contains_key(path)
}

pub fn asset_registry_name_exists(registry: &AssetRegistry, name: &str) -> bool {
    registry.name_to_uuid.contains_key(name)
}

pub fn asset_registry_clear(registry: &mut AssetRegistry) {
    registry.manifests.clear();
    registry.uuid_to_manifest.clear();
    registry.path_to_uuid.clear();
    registry.name_to_uuid.clear();
    registry.base_paths.clear();
}

pub fn asset_registry_manifest_count(registry: &AssetRegistry) -> usize {
    registry.manifests.len()
}

pub fn asset_registry_asset_count(registry: &AssetRegistry) -> usize {
    registry
        .manifests
        .iter()
        .map(|manifest| manifest.len())
        .sum()
}

#[derive(Debug, Clone)]
pub struct RuntimeAssetEntry {
    pub uuid: AssetUuid,
    pub name: String,
    pub asset_type: AssetType,
    pub loaded: bool,
}

#[derive(Debug, Clone, Default)]
pub struct RuntimeAssetRegistry {
    pub entries: HashMap<AssetUuid, RuntimeAssetEntry>,
    pub name_to_uuid: HashMap<String, AssetUuid>,
    pub type_to_uuids: HashMap<AssetType, Vec<AssetUuid>>,
}

pub fn runtime_asset_registry_register(
    registry: &mut RuntimeAssetRegistry,
    uuid: AssetUuid,
    name: impl Into<String>,
    asset_type: AssetType,
) {
    let name = name.into();
    let entry = RuntimeAssetEntry {
        uuid,
        name: name.clone(),
        asset_type,
        loaded: false,
    };

    registry.name_to_uuid.insert(name, uuid);
    registry
        .type_to_uuids
        .entry(asset_type)
        .or_default()
        .push(uuid);
    registry.entries.insert(uuid, entry);
}

pub fn runtime_asset_registry_mark_loaded(registry: &mut RuntimeAssetRegistry, uuid: AssetUuid) {
    if let Some(entry) = registry.entries.get_mut(&uuid) {
        entry.loaded = true;
    }
}

pub fn runtime_asset_registry_mark_unloaded(registry: &mut RuntimeAssetRegistry, uuid: AssetUuid) {
    if let Some(entry) = registry.entries.get_mut(&uuid) {
        entry.loaded = false;
    }
}

pub fn runtime_asset_registry_is_loaded(registry: &RuntimeAssetRegistry, uuid: AssetUuid) -> bool {
    registry
        .entries
        .get(&uuid)
        .is_some_and(|entry| entry.loaded)
}

pub fn runtime_asset_registry_get(
    registry: &RuntimeAssetRegistry,
    uuid: AssetUuid,
) -> Option<&RuntimeAssetEntry> {
    registry.entries.get(&uuid)
}

pub fn runtime_asset_registry_get_by_name<'a>(
    registry: &'a RuntimeAssetRegistry,
    name: &str,
) -> Option<&'a RuntimeAssetEntry> {
    registry
        .name_to_uuid
        .get(name)
        .and_then(|uuid| registry.entries.get(uuid))
}

pub fn runtime_asset_registry_resolve_name(
    registry: &RuntimeAssetRegistry,
    name: &str,
) -> Option<AssetUuid> {
    registry.name_to_uuid.get(name).copied()
}

pub fn runtime_asset_registry_unregister(registry: &mut RuntimeAssetRegistry, uuid: AssetUuid) {
    if let Some(entry) = registry.entries.remove(&uuid) {
        registry.name_to_uuid.remove(&entry.name);
        if let Some(uuids) = registry.type_to_uuids.get_mut(&entry.asset_type) {
            uuids.retain(|other| *other != uuid);
        }
    }
}

pub fn runtime_asset_registry_clear(registry: &mut RuntimeAssetRegistry) {
    registry.entries.clear();
    registry.name_to_uuid.clear();
    registry.type_to_uuids.clear();
}
