// engine
use engine::prelude::*;

use assetpath::AssetPath;

// use lua_wrapper::LuaFunction;

use anyhow::Result;
use entity_manager::*;
use lua_wrapper::LuaFunction;
use rpg_components::config::abilities::AbilitySettings;
use rpg_components::config::attributes::AttributeSettings;
use rpg_components::config::experience::ExperienceSettings;
use rpg_components::config::items::ItemSettings;
use rpg_components::items::{ItemSystem, ToolTipBuilder};

// std
use std::collections::HashMap;
use std::sync::{Mutex, MutexGuard, RwLock};
use std::{fs::create_dir_all, path::Path, sync::Arc};

// game
use crate::game::configloader::*;
use crate::loader::settings::*;

use cgmath::Vector3;
use promise::Promise;

use super::handle::{StrongHandle, WeakHandle};

#[derive(Default, Debug, Clone)]
pub struct MapInformation {
    // root directories for maps
    pub map_directories: Vec<AssetPath>,

    // possible start map, where players get spawned on connection
    pub start_map: Option<String>,

    // maps that are going to be ignored
    pub black_list: Vec<String>,
}

pub struct Game {
    engine: Arc<Engine>,

    game_settings: GameSection,
    _lua_scripts: LuaScripts,

    data_directory: String,

    pub attribute_settings: AttributeSettings,
    pub item_settings: ItemSettings,
    pub experience_settings: ExperienceSettings,
    pub mob_settings: MobSettings,
    pub ability_settings: AbilitySettings,

    pub maps: RwLock<HashMap<String, AssetPath>>,
    pub black_listed_maps: RwLock<HashMap<String, AssetPath>>,
    pub tiles: RwLock<HashMap<String, AssetPath>>,

    settings_file_location: AssetPath,

    item_system: Promise<Arc<ItemSystem>>,

    entity_manager: Mutex<EntityManager>,

    slide_images: Mutex<HashMap<String, Arc<Image>>>,

    pub change_map_context: LuaFunction,

    map_info: MapInformation,
}

pub type GameHandle = WeakHandle<Game>;
pub type StrongGame = StrongHandle<Game>;

impl GameHandle {
    pub fn gui_builder(&self, path: &str) -> Result<Arc<GuiBuilder>> {
        let strong = self.upgrade();

        GuiBuilder::new(strong.engine.gui_handler(), &strong.build_data_path(path))
    }

    pub fn gui_snippet(&self, path: &str) -> Result<Arc<GuiSnippet>> {
        let strong = self.upgrade();

        GuiSnippet::new(strong.engine.gui_handler(), &strong.build_data_path(path))
    }

    pub fn controller_icon(&self, button: ControllerButton) -> Result<Arc<Image>> {
        let game = self.upgrade();
        let engine = game.engine();

        engine
            .settings()
            .controller_button_for(engine, button, ControllerType::XBox)
    }

    pub fn build_data_path(&self, path: &str) -> AssetPath {
        self.upgrade().build_data_path(path)
    }
}

impl ToolTipBuilder for GameHandle {
    fn asset_path(&self, s: &str) -> AssetPath {
        self.build_data_path(s)
    }

    fn gui_handler(&self) -> Arc<GuiHandler> {
        self.upgrade().engine().gui_handler().clone()
    }
}

pub struct Settings {
    pub core_settings: CoreSettings,
    pub user_settings: UserSettings,
    pub data_path: String,
    pub user_settings_file: AssetPath,
}

impl Game {
    pub fn load_settings<'a>(data_path: impl Into<Option<&'a str>>) -> Result<Settings> {
        let data_path = data_path.into().unwrap_or("data");

        println!("Data directory: {}", data_path);

        let mut core_settings = CoreSettings::load((data_path, "settings.conf"))?;
        let mut user_settings = UserSettings::load((data_path, "user_settings.conf"))?;

        let gavania_save_dir = settings_file_dir();
        Self::verify_dir_existence(&gavania_save_dir)?;

        let user_settings_file = AssetPath::from((gavania_save_dir, "user_settings.conf"));

        if user_settings_file.exists() {
            user_settings.load_with_default(user_settings_file.clone())?;
        } else {
            user_settings.file_name = user_settings_file.clone();
        }

        user_settings.store()?;

        Self::apply_data_dir(&mut core_settings, data_path);

        Ok(Settings {
            core_settings,
            user_settings,
            data_path: data_path.to_string(),
            user_settings_file,
        })
    }

    pub fn new(settings: Settings) -> Result<StrongGame> {
        #[cfg(target_os = "linux")]
        {
            Self::set_radeon_ray_tracing();

            #[cfg(feature = "wayland")]
            Self::check_wayland_for_sdl();
        }

        let game_settings = settings.core_settings.game.clone();

        let lua_scripts = settings.core_settings.lua_scripts.clone();

        let tile_directory = settings.core_settings.engine.tile_directory.full_path();
        let map_info = settings.core_settings.map_info();

        #[allow(unused_mut)]
        let mut engine_create_info =
            into_engine_info(settings.core_settings, settings.user_settings);

        {
            engine_create_info.app_info.application_name = "Gavania".to_string();
            engine_create_info.app_info.application_version = 1;
        }

        let attribute_settings =
            AttributeSettings::load((settings.data_path.as_str(), "configs/stats.conf"))?;
        let mut item_settings =
            ItemSettings::load((settings.data_path.as_str(), "configs/items.conf"))?;
        let experience_settings =
            ExperienceSettings::load((settings.data_path.as_str(), "configs/experience.conf"))?;
        let mob_settings = MobSettings::load((settings.data_path.as_str(), "configs/mobs.conf"))?;
        let mut ability_settings =
            AbilitySettings::load((settings.data_path.as_str(), "configs/abilities.conf"))?;

        Self::apply_data_dir_to_configs(
            &mut item_settings,
            &mut ability_settings,
            &settings.data_path,
        );

        // -----------------  maps  ---------------------
        let mut map_map = HashMap::new();
        let mut black_listed_maps = HashMap::new();

        for map_directory in map_info.map_directories.iter() {
            let map_vector = search_dir_recursively(&map_directory.full_path(), "")?;

            'outer: for file in &map_vector {
                let s = file.full_path();
                let path = Path::new(s.as_str());

                if let Some(name) = path.file_stem() {
                    if let Some(name_str) = name.to_str() {
                        let string = name_str.to_string();

                        for blacked in map_info.black_list.iter() {
                            if *blacked == string {
                                assert!(
                                    black_listed_maps
                                        .insert(string.clone(), file.clone())
                                        .is_none(),
                                    "blacklisted map already present: {}",
                                    string
                                );

                                continue 'outer;
                            }
                        }

                        assert!(
                            map_map.insert(string.clone(), file.clone()).is_none(),
                            "map already present: {}",
                            string
                        );
                    }
                }
            }
        }

        // -----------------  tiles  --------------------
        let tile_vector = search_dir_recursively(&tile_directory, ".png")?;
        let mut tile_map = HashMap::new();

        for file in &tile_vector {
            let s = file.full_path();
            let path = Path::new(s.as_str());

            if let Some(name) = path.file_stem() {
                if let Some(name_str) = name.to_str() {
                    let string = name_str.to_string();

                    tile_map.insert(string.clone(), file.clone());
                }
            }
        }

        let game = {
            engine_create_info.gui_info.resource_directory = {
                let mut path = AssetPath::from(settings.data_path.as_str());
                path.assume_prefix_free();

                path
            };
            engine_create_info.resource_base_path = settings.data_path.clone();

            let engine = Engine::new(engine_create_info.clone())?;

            let item_system = Promise::new({
                let engine = engine.clone();
                let item_settings = item_settings.clone();
                let ability_settings = ability_settings.clone();
                let attribute_settings = attribute_settings.clone();
                let ability_directory = game_settings.ability_directory.clone();
                let data_path = settings.data_path.clone();

                move || {
                    Ok(Arc::new(ItemSystem::new(
                        &engine,
                        &item_settings,
                        &ability_settings,
                        &attribute_settings,
                        &ability_directory,
                        &data_path,
                    )?))
                }
            });

            StrongHandle::new(Game {
                engine,

                maps: RwLock::new(map_map),
                black_listed_maps: RwLock::new(black_listed_maps),
                tiles: RwLock::new(tile_map),

                change_map_context: LuaFunction::new(&lua_scripts.change_map)?,
                game_settings,
                _lua_scripts: lua_scripts,

                data_directory: settings.data_path.to_string(),

                attribute_settings,
                item_settings,
                experience_settings,
                mob_settings,
                ability_settings,

                item_system,

                entity_manager: Mutex::new(EntityManager::new(
                    engine_create_info.asset_directories.entity_file_directory,
                )?),

                settings_file_location: settings.user_settings_file,

                slide_images: Mutex::new(HashMap::new()),

                map_info,
            })
        };

        Ok(game)
    }

    fn verify_dir_existence(dir: &str) -> Result<()> {
        let path = Path::new(&dir);

        if !path.exists() {
            create_dir_all(path)?;
        }

        Ok(())
    }

    pub fn run(&self) -> Result<()> {
        self.engine.run()
    }

    pub fn set_game_state<E>(&self, game_object: E) -> Result<()>
    where
        E: EngineObject + Send + Sync,
    {
        self.engine.set_game_object(Some(game_object))
    }

    pub fn quit(&self) -> Result<()> {
        self.store_settings()?;

        self.engine.quit()?;

        Ok(())
    }

    #[cfg(target_os = "linux")]
    fn set_radeon_ray_tracing() {
        std::env::set_var("RADV_PERFTEST", "rt");
    }

    #[cfg(feature = "wayland")]
    #[cfg(target_os = "linux")]
    fn check_wayland_for_sdl() {
        if let Ok(session_type) = std::env::var("XDG_SESSION_TYPE") {
            if session_type == "wayland" {
                std::env::set_var("SDL_VIDEODRIVER", "wayland");
            }
        }
    }

    pub fn map_directories(&self) -> &[AssetPath] {
        &self.map_info.map_directories
    }

    pub fn home_map(&self) -> Option<&String> {
        self.map_info.start_map.as_ref()
    }

    pub fn black_listed_maps(&self) -> &[String] {
        &self.map_info.black_list
    }

    pub fn engine(&self) -> &Arc<Engine> {
        &self.engine
    }

    pub fn game_settings(&self) -> &GameSection {
        &self.game_settings
    }

    pub fn item_system(&self) -> Arc<ItemSystem> {
        self.item_system.get()
    }

    pub fn data_directory(&self) -> &str {
        &self.data_directory
    }

    pub fn build_data_path(&self, path: &str) -> AssetPath {
        AssetPath::from((self.data_directory.as_str(), path))
    }

    pub fn entity_manager(&self) -> MutexGuard<'_, EntityManager> {
        self.entity_manager.lock().unwrap()
    }

    pub fn slide_images(&self) -> MutexGuard<'_, HashMap<String, Arc<Image>>> {
        self.slide_images.lock().unwrap()
    }

    pub fn gui_builder(&self, path: &str) -> Result<Arc<GuiBuilder>> {
        GuiBuilder::new(self.engine.gui_handler(), &self.build_data_path(path))
    }

    pub fn gui_snippet(&self, path: &str) -> Result<Arc<GuiSnippet>> {
        GuiSnippet::new(self.engine.gui_handler(), &self.build_data_path(path))
    }

    pub fn store_settings(&self) -> Result<()> {
        let mut user_settings = UserSettings::load(self.settings_file_location.clone())?;

        // first update settings
        if self.engine.window_config().is_fullscreen() {
            user_settings.window.fullscreen = true;
        } else {
            user_settings.window.fullscreen = false;
            let render_core = self.engine.context().render_core();

            user_settings.window.width = render_core.width();
            user_settings.window.height = render_core.height();
        }

        {
            let sound = self.engine.sound();

            user_settings.audio.master = sound.volume("master")?;
            user_settings.audio.gui = sound.volume("gui")?;
            user_settings.audio.sfx = sound.volume("sfx")?;
            user_settings.audio.music = sound.volume("music")?;
        }

        // check if renderer type actually changed
        // if so, clear all loaded entities
        let renderer_type = self.engine.settings().graphics_info()?.render_type;

        if renderer_type != user_settings.graphics.renderer_type {
            user_settings.graphics.renderer_type = renderer_type;

            self.engine.assets().clear()?;
        }

        user_settings.store()?;

        Ok(())
    }

    pub fn set_frustum_check(scene: &mut Scene) {
        let entity_name = "Lightning".to_string();

        scene.set_frustum_check(move |entity, _view_proj, _view_frustum, frustum_planes| {
            // filter everything that isnt Draw
            if !entity.contains_component::<Draw>() {
                if entity.debug_name.as_ref() == Some(&entity_name) {
                    println!("{entity_name}-Entity does not contain Draw");
                }

                return Ok(false);
            }

            // filter everything that isnt Bounding Box
            let bb = match entity.get_component::<BoundingBox>() {
                Ok(bounding_box) => bounding_box,
                Err(_) => {
                    if entity.debug_name.as_ref() == Some(&entity_name) {
                        println!("{entity_name}-Entity does not contain BoundingBox");
                    }

                    return Ok(false);
                }
            };

            let transformation_matrix = entity
                .get_component::<Location>()
                .ok()
                .map(|location| location.transformation_matrix());

            let planes = frustum_planes.as_array();
            let corners: Vec<Vector3<f32>> = bb
                .corners()
                .iter()
                .map(|corner| match transformation_matrix {
                    Some(m) => (m * corner.extend(1.0)).xyz(),
                    None => *corner,
                })
                .collect();

            // if there is a plane where all corners are outside, the box is not visible
            for plane in planes {
                let mut outside = true;

                for corner in corners.iter() {
                    if !plane.is_above(*corner) {
                        outside = false;
                        break;
                    }
                }

                if outside {
                    return Ok(false);
                }
            }

            if entity.debug_name.as_ref() == Some(&entity_name) {
                println!("{entity_name}-Entity is inside");
            }

            Ok(true)
        });
    }

    fn apply_data_dir(settings: &mut CoreSettings, data_dir: &str) {
        // game
        settings.game.sounds_directory.set_prefix(data_dir);
        settings.game.music_directory.set_prefix(data_dir);
        settings.game.ability_icon_directory.set_prefix(data_dir);
        settings.game.ability_directory.set_prefix(data_dir);
        settings.game.particle_directory.set_prefix(data_dir);
        settings.game.slides_directory.set_prefix(data_dir);
        settings.game.npc_directory.set_prefix(data_dir);

        // map info
        settings.map_infos.directory.set_prefix(data_dir);

        // scripts
        settings.lua_scripts.change_map.set_prefix(data_dir);
    }

    fn apply_data_dir_to_configs(
        item_settings: &mut ItemSettings,
        ability_settings: &mut AbilitySettings,
        data_dir: &str,
    ) {
        item_settings.icon_paths.amulet.set_prefix(data_dir);
        item_settings.icon_paths.background.set_prefix(data_dir);
        item_settings.icon_paths.boots.set_prefix(data_dir);
        item_settings.icon_paths.chest.set_prefix(data_dir);
        item_settings.icon_paths.helmet.set_prefix(data_dir);
        item_settings.icon_paths.jewel.set_prefix(data_dir);
        item_settings.icon_paths.ring.set_prefix(data_dir);
        item_settings.icon_paths.belt.set_prefix(data_dir);
        item_settings.icon_paths.gloves.set_prefix(data_dir);
        item_settings.icon_paths.main_hand.set_prefix(data_dir);
        item_settings.icon_paths.off_hand.set_prefix(data_dir);

        item_settings.jewel_paths.first.set_prefix(data_dir);
        item_settings.jewel_paths.second.set_prefix(data_dir);
        item_settings.jewel_paths.third.set_prefix(data_dir);
        item_settings.jewel_paths.fourth.set_prefix(data_dir);

        ability_settings.icons.book.set_prefix(data_dir);
        ability_settings.icons.damage.set_prefix(data_dir);
        ability_settings.icons.projectile_speed.set_prefix(data_dir);
        ability_settings.icons.bounce.set_prefix(data_dir);
        ability_settings.icons.explosion.set_prefix(data_dir);
        ability_settings.icons.size.set_prefix(data_dir);
        ability_settings
            .icons
            .additional_projectiles
            .set_prefix(data_dir);
        ability_settings.icons.cool_down.set_prefix(data_dir);
        ability_settings.icons.distance.set_prefix(data_dir);
        ability_settings.icons.addon_background.set_prefix(data_dir);
    }

    fn gather_maps(
        map_directories: &[AssetPath],
        home_map: Option<&str>,
        ignore_maps: &[String],
    ) -> Result<Vec<String>> {
        let mut maps = Vec::new();

        for map_directory in map_directories.iter() {
            for map in search_dir_recursively(&map_directory.full_path(), "")? {
                let map_name = Path::new(&map.full_path())
                    .file_name()
                    .expect("map is not a file name")
                    .to_str()
                    .expect("failed converting OsStr to str")
                    .to_string();

                if let Some(home) = home_map {
                    if map_name == home {
                        continue;
                    }
                }

                if ignore_maps.contains(&map_name) {
                    continue;
                }

                maps.push(map_name);
            }
        }

        Ok(maps)
    }

    pub fn next_map(&self, current_map: &str) -> Result<String> {
        let map_directories = self.map_directories();
        let home_map = self.home_map().map(|s| s.as_str());
        let ignore_maps: Vec<String> = self
            .black_listed_maps
            .read()
            .unwrap()
            .keys()
            .cloned()
            .collect();

        let maps = Self::gather_maps(map_directories, home_map, &ignore_maps)?;
        let map_count = maps.len();

        let next_map = self.change_map_context.execute((
            current_map,
            home_map.unwrap_or(""),
            maps,
            map_count,
        ))?;

        Ok(next_map)
    }
}

impl Drop for Game {
    fn drop(&mut self) {}
}