use crate::prelude::*;
use bevy::asset::AsyncReadExt;
use bevy::utils::HashMap;
use bevy::{
    asset::{io::Reader, AssetLoader, LoadContext},
    reflect::TypePath,
    utils::BoxedFuture,
};
use serde::Deserialize;
use std::str::Utf8Error;
use thiserror::Error;

/// A Tweaks is resource used to specify game customization like asset names,
/// and non-user customizations made to the game during development.
pub(crate) struct TweakPlugin;

impl Plugin for TweakPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(OnEnter(GameState::Loading), load_tweakfile);

        app.init_asset::<Tweaks>()
            .register_asset_loader(TweaksLoader);
    }
}

fn load_tweakfile(server: Res<AssetServer>, mut commands: Commands) {
    let handle: Handle<Tweaks> = server.load("martian.tweak.toml");
    commands.insert_resource(GameTweaks { handle });
}

#[derive(Debug, Resource)]
pub(crate) struct GameTweaks {
    pub handle: Handle<Tweaks>,
}

#[derive(Debug, Asset, TypePath)]
pub struct Tweaks {
    table: toml::Table,
    handles: HashMap<String, UntypedHandle>,
}

impl Tweaks {
    fn from_table(table: &toml::Table, load_context: &mut LoadContext) -> Tweaks {
        let handles = Tweaks::iter_all(table, "")
            .iter()
            .filter_map(|(k, v)| match v {
                toml::Value::String(s) => std::path::Path::new(format!("assets/{}", s).as_str())
                    .exists()
                    .then(|| {
                        if s.ends_with(".gltf") || s.ends_with(".glb") {
                            Some(load_context.load::<Gltf>(s).untyped())
                        } else if s.ends_with(".png") {
                            Some(load_context.load::<Image>(s).untyped())
                        } else {
                            None
                        }
                    })
                    .flatten()
                    .map(|h| (k.clone(), h)),
                _ => None,
            })
            .collect();

        Tweaks {
            table: table.clone(),
            handles,
        }
    }

    pub fn get_handle<T: Asset>(&self, key: &str) -> Option<Handle<T>> {
        self.handles.get(key).map(|h| h.clone().typed::<T>())
    }

    pub fn get_handle_unchecked<T: Asset>(&self, key: &str) -> Option<Handle<T>> {
        self.handles
            .get(key)
            .map(|h| h.clone().typed_unchecked::<T>())
    }

    pub fn get<'de, T: Deserialize<'de>>(&self, key: &str) -> Option<T> {
        Tweaks::locate(&self.table, key).map(|val| match val.try_into() {
            Ok(val) => val,
            Err(e) => panic!("{}", e.message()),
        })
    }

    fn iter_all(t: &toml::Table, key: &str) -> Vec<(String, toml::Value)> {
        t.iter()
            .flat_map(|(k, v)| {
                let nk = if key == "" {
                    k.to_string()
                } else {
                    format!("{}_{}", key, k)
                };
                match v {
                    toml::Value::Table(nt) => Tweaks::iter_all(nt, nk.as_str()),
                    _ => vec![(nk, v.clone())],
                }
            })
            .collect()
    }

    fn locate(t: &toml::Table, key: &str) -> Option<toml::Value> {
        t.iter().find_map(|(k, v)| {
            if key == k {
                Some(v.clone())
            } else if key.starts_with(k) {
                let prefix = format!("{}_", k);
                match v {
                    toml::Value::Table(nt) => {
                        Tweaks::locate(nt, key.strip_prefix(prefix.as_str()).unwrap())
                    }
                    _ => Some(v.clone()),
                }
            } else {
                None
            }
        })
    }
}

#[derive(Default)]
pub struct TweaksLoader;

#[derive(Debug, Error)]
pub enum TweaksError {
    #[error("Failed to read file")]
    IO(#[from] std::io::Error),
    #[error("Failed to decode file")]
    Decode(#[from] Utf8Error),
    #[error("Failed to parse file")]
    Parse(#[from] toml::de::Error),
}

impl AssetLoader for TweaksLoader {
    type Asset = Tweaks;
    type Settings = ();
    type Error = TweaksError;

    fn load<'a>(
        &'a self,
        reader: &'a mut Reader,
        _settings: &'a Self::Settings,
        load_context: &'a mut LoadContext,
    ) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
        Box::pin(async move {
            let mut bytes = Vec::new();
            reader.read_to_end(&mut bytes).await?;
            let s = std::str::from_utf8(bytes.as_slice())?;
            match toml::from_str::<toml::Table>(s) {
                Ok(parsed) => {
                    let result = Tweaks::from_table(&parsed, load_context);
                    Ok(result)
                }
                Err(e) => {
                    println!("Error parsing tweaks: {:?}", e.message());
                    Err(TweaksError::Parse(e))
                }
            }
        })
    }

    fn extensions(&self) -> &[&str] {
        &["tweak.toml"]
    }
}
