pub mod anim_texture;
pub mod atlas_texture;
pub mod font;
pub mod gltf;
pub mod ldtk;
pub mod shader;
pub mod sound;
pub mod spine;
pub mod texture;

use crate::assets::{
    anim_texture::make_anim_texture_asset_protocol,
    atlas_texture::make_atlas_texture_asset_protocol, font::FontAssetProtocol,
    gltf::make_gltf_asset_protocol, ldtk::LdtkAssetProtocol, shader::ShaderAssetProtocol,
    sound::SoundAssetProtocol, spine::SpineAssetProtocol, texture::TextureAssetProtocol,
};
use keket::{
    database::{
        AssetDatabase,
        handle::AssetHandle,
        path::{AssetPath, AssetPathStatic},
    },
    fetch::{
        AssetFetch,
        container::{ContainerAssetFetch, ContainerPartialFetch},
        throttled::{ThrottledAssetFetch, ThrottledAssetFetchStrategy},
    },
    protocol::{bytes::BytesAssetProtocol, group::GroupAssetProtocol, text::TextAssetProtocol},
};
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    error::Error,
    hash::{DefaultHasher, Hash, Hasher},
    io::{Cursor, Read, Write},
    ops::Range,
    path::Path,
};

pub fn name_from_path<'a>(path: &'a AssetPath<'a>) -> &'a str {
    path.meta_items()
        .find(|(key, _)| *key == "as")
        .map(|(_, value)| value)
        .unwrap_or(path.path())
}

pub fn find_asset_by_name(database: &AssetDatabase, name: &str) -> Option<AssetHandle> {
    database
        .storage
        .find_with::<true, AssetPathStatic>(|path| name_from_path(path) == name)
        .map(AssetHandle::new)
}

pub fn make_database_protocols() -> AssetDatabase {
    AssetDatabase::default()
        .with_protocol(BytesAssetProtocol)
        .with_protocol(TextAssetProtocol)
        .with_protocol(GroupAssetProtocol)
        .with_protocol(ShaderAssetProtocol)
        .with_protocol(TextureAssetProtocol)
        .with_protocol(make_anim_texture_asset_protocol())
        .with_protocol(FontAssetProtocol)
        .with_protocol(SoundAssetProtocol)
        .with_protocol(SpineAssetProtocol)
        .with_protocol(LdtkAssetProtocol)
        .with_protocol(make_gltf_asset_protocol())
        .with_protocol(make_atlas_texture_asset_protocol())
}

pub fn make_database(fetch: impl AssetFetch) -> AssetDatabase {
    make_database_protocols().with_fetch(fetch)
}

pub fn make_memory_database(package: &[u8]) -> Result<AssetDatabase, Box<dyn Error>> {
    Ok(make_database(ContainerAssetFetch::new(
        AssetPackage::decode(package)?,
    )))
}

pub fn make_throttled_memory_database(
    package: &[u8],
    strategy: ThrottledAssetFetchStrategy,
) -> Result<AssetDatabase, Box<dyn Error>> {
    Ok(make_database(ThrottledAssetFetch::new(
        ContainerAssetFetch::new(AssetPackage::decode(package)?),
        strategy,
    )))
}

pub fn make_directory_database(
    directory: impl AsRef<Path>,
) -> Result<AssetDatabase, Box<dyn Error>> {
    Ok(make_database(ContainerAssetFetch::new(
        AssetPackage::from_directory(directory)?,
    )))
}

pub fn make_throttled_directory_database(
    directory: impl AsRef<Path>,
    strategy: ThrottledAssetFetchStrategy,
) -> Result<AssetDatabase, Box<dyn Error>> {
    Ok(make_database(ThrottledAssetFetch::new(
        ContainerAssetFetch::new(AssetPackage::from_directory(directory)?),
        strategy,
    )))
}

pub fn make_replacement_package_filter(
    items: &[(&str, Option<&str>)],
) -> impl Fn(&Path) -> Option<String> {
    move |path| {
        let name = path.file_name()?.to_string_lossy().to_string();
        for (subextension, replacement) in items {
            if name.contains(subextension) {
                let replacement = replacement.as_deref()?;
                return Some(name.replace(subextension, replacement));
            }
        }
        Some(name)
    }
}

#[derive(Debug, Default, Serialize, Deserialize)]
struct AssetPackageRegistry {
    mappings: HashMap<String, Range<usize>>,
}

#[derive(Default)]
pub struct AssetPackage {
    registry: AssetPackageRegistry,
    content: Vec<u8>,
}

impl AssetPackage {
    pub fn from_directory(directory: impl AsRef<Path>) -> Result<Self, Box<dyn Error>> {
        Self::from_directory_filtered(directory, |path| {
            Some(path.file_name()?.to_string_lossy().to_string())
        })
    }

    pub fn from_directory_filtered(
        directory: impl AsRef<Path>,
        filter: impl Fn(&Path) -> Option<String>,
    ) -> Result<Self, Box<dyn Error>> {
        fn visit_dirs(
            dir: &Path,
            root: &str,
            registry: &mut AssetPackageRegistry,
            content: &mut Cursor<Vec<u8>>,
            filter: &impl Fn(&Path) -> Option<String>,
        ) -> std::io::Result<()> {
            if dir.is_dir() {
                for entry in std::fs::read_dir(dir)? {
                    let entry = entry?;
                    let path = entry.path();
                    let name = path.file_name().unwrap().to_str().unwrap();
                    if path.is_dir() {
                        let name = if root.is_empty() {
                            name.to_owned()
                        } else {
                            format!("{root}/{name}")
                        };
                        visit_dirs(&path, &name, registry, content, filter)?;
                    } else if let Some(name) = filter(&path) {
                        let name = if root.is_empty() {
                            name.to_owned()
                        } else {
                            format!("{root}/{name}")
                        };
                        let bytes = std::fs::read(path)?;
                        let start = content.position() as usize;
                        content.write_all(&bytes)?;
                        let end = content.position() as usize;
                        registry.mappings.insert(name, start..end);
                    }
                }
            }
            Ok(())
        }

        let directory = directory.as_ref();
        let mut registry = AssetPackageRegistry::default();
        let mut content = Cursor::new(Vec::default());
        visit_dirs(directory, "", &mut registry, &mut content, &filter)?;
        Ok(AssetPackage {
            registry,
            content: content.into_inner(),
        })
    }

    pub fn decode(bytes: &[u8]) -> Result<Self, Box<dyn Error>> {
        let mut stream = Cursor::new(bytes);
        let mut size = 0u32.to_be_bytes();
        stream.read_exact(&mut size)?;
        let size = u32::from_be_bytes(size) as usize;
        let mut registry = vec![0u8; size];
        stream.read_exact(&mut registry)?;
        let registry = toml::from_str(std::str::from_utf8(&registry)?)?;
        let mut content = Vec::default();
        stream.read_to_end(&mut content)?;
        Ok(Self { registry, content })
    }

    pub fn encode(&self) -> Result<Vec<u8>, Box<dyn Error>> {
        let mut stream = Cursor::new(Vec::default());
        let registry = toml::to_string(&self.registry)?;
        let registry = registry.as_bytes();
        stream.write_all(&(registry.len() as u32).to_be_bytes())?;
        stream.write_all(registry)?;
        stream.write_all(&self.content)?;
        Ok(stream.into_inner())
    }

    pub fn paths(&self) -> impl Iterator<Item = &str> {
        self.registry.mappings.keys().map(|key| key.as_str())
    }

    pub fn paths_and_content_hashes(&self) -> impl Iterator<Item = (&str, u64)> {
        self.registry.mappings.iter().map(move |(key, range)| {
            let mut hasher = DefaultHasher::new();
            self.content[range.clone()].hash(&mut hasher);
            (key.as_str(), hasher.finish())
        })
    }
}

impl ContainerPartialFetch for AssetPackage {
    fn load_bytes(&mut self, path: AssetPath) -> Result<Vec<u8>, Box<dyn Error>> {
        if let Some(range) = self.registry.mappings.get(path.path()).cloned() {
            if range.end <= self.content.len() {
                Ok(self.content[range].to_owned())
            } else {
                Err(format!(
                    "Asset: `{}` out of content bounds! Bytes range: {:?}, content byte size: {}",
                    path,
                    range,
                    self.content.len()
                )
                .into())
            }
        } else {
            Err(format!("Asset: `{path}` not present in package!").into())
        }
    }
}

impl std::fmt::Debug for AssetPackage {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AssetPackage")
            .field("registry", &self.registry)
            .finish_non_exhaustive()
    }
}
