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

use freecs::Entity;
use rhai::{AST, Engine, Scope};
use winit::keyboard::KeyCode;

struct CachedScript {
    ast: AST,
    source_hash: u64,
}

pub struct ScriptRuntime {
    pub engine: Engine,
    compiled_scripts: HashMap<String, CachedScript>,
    pub entity_scopes: HashMap<Entity, Scope<'static>>,
    pub previous_keys: HashSet<KeyCode>,
    pub accumulated_time: f64,
    pub game_state: HashMap<String, f64>,
    #[cfg(not(target_arch = "wasm32"))]
    pub file_modification_times: HashMap<String, std::time::SystemTime>,
}

impl Default for ScriptRuntime {
    fn default() -> Self {
        Self::new()
    }
}

impl ScriptRuntime {
    pub fn new() -> Self {
        let engine = Engine::new();

        Self {
            engine,
            compiled_scripts: HashMap::new(),
            entity_scopes: HashMap::new(),
            previous_keys: HashSet::new(),
            accumulated_time: 0.0,
            game_state: HashMap::new(),
            #[cfg(not(target_arch = "wasm32"))]
            file_modification_times: HashMap::new(),
        }
    }

    pub fn get_or_create_scope(&mut self, entity: Entity) -> &mut Scope<'static> {
        self.entity_scopes.entry(entity).or_default()
    }

    pub fn remove_entity_scope(&mut self, entity: Entity) {
        self.entity_scopes.remove(&entity);
    }

    pub fn compile_script(&mut self, key: &str, source: &str) -> Result<&AST, String> {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        source.hash(&mut hasher);
        let source_hash = hasher.finish();

        let needs_compile = match self.compiled_scripts.get(key) {
            Some(cached) => cached.source_hash != source_hash,
            None => true,
        };

        if needs_compile {
            let ast = self
                .engine
                .compile(source)
                .map_err(|e| format!("Compilation error: {}", e))?;
            self.compiled_scripts
                .insert(key.to_string(), CachedScript { ast, source_hash });
        }
        Ok(&self.compiled_scripts.get(key).unwrap().ast)
    }

    pub fn invalidate_script(&mut self, key: &str) {
        self.compiled_scripts.remove(key);
    }

    pub fn reset_time(&mut self) {
        self.accumulated_time = 0.0;
    }

    pub fn reset_game_state(&mut self) {
        self.game_state.clear();
    }

    pub fn get_state(&self, key: &str) -> f64 {
        self.game_state.get(key).copied().unwrap_or(0.0)
    }

    pub fn set_state(&mut self, key: String, value: f64) {
        self.game_state.insert(key, value);
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn check_hot_reload(&mut self) -> Vec<String> {
        let mut reloaded = Vec::new();

        let paths: Vec<String> = self.file_modification_times.keys().cloned().collect();
        for path in paths {
            if let Ok(metadata) = std::fs::metadata(&path)
                && let Ok(modified) = metadata.modified()
                && let Some(cached_time) = self.file_modification_times.get(&path)
                && modified > *cached_time
            {
                self.compiled_scripts.remove(&path);
                self.file_modification_times.insert(path.clone(), modified);
                reloaded.push(path);
            }
        }

        reloaded
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn track_file(&mut self, path: &str) {
        if let Ok(metadata) = std::fs::metadata(path)
            && let Ok(modified) = metadata.modified()
        {
            self.file_modification_times
                .insert(path.to_string(), modified);
        }
    }
}
