use freecs::Entity;
use rhai::Scope;
use winit::keyboard::KeyCode;

use super::api::register_api;
use super::components::{Script, ScriptSource};
use super::resources::ScriptRuntime;
use crate::ecs::world::commands::{WorldCommand, spawn_cube_at, spawn_sphere_at};
use crate::ecs::world::{SCRIPT, World};

pub fn run_scripts_system(world: &mut World, runtime: &mut ScriptRuntime) {
    let _span = tracing::info_span!("scripts").entered();

    register_api(&mut runtime.engine);

    let delta_time = world.resources.window.timing.delta_time;
    runtime.accumulated_time += delta_time as f64;

    let pressed_keys = build_pressed_keys(world);
    let just_pressed_keys = build_just_pressed_keys(world, runtime);
    let mouse_position = {
        let mouse = &world.resources.input.mouse;
        (mouse.position.x as f64, mouse.position.y as f64)
    };

    #[cfg(not(target_arch = "wasm32"))]
    {
        let reloaded = runtime.check_hot_reload();
        for path in reloaded {
            tracing::info!("Hot-reloaded script: {}", path);
        }
    }

    let entities: Vec<Entity> = world.query_entities(SCRIPT).collect();

    let mut commands: Vec<(Entity, ScriptResult)> = Vec::new();

    for entity in entities {
        let script = match world.get_script(entity) {
            Some(s) => s.clone(),
            None => continue,
        };

        if !script.enabled {
            continue;
        }

        let transform = world
            .get_local_transform(entity)
            .copied()
            .unwrap_or_default();

        let euler = quat_to_euler(&transform.rotation);

        let mut scope = Scope::new();
        scope.push("pos_x", transform.translation.x as f64);
        scope.push("pos_y", transform.translation.y as f64);
        scope.push("pos_z", transform.translation.z as f64);
        scope.push("rot_x", euler.0 as f64);
        scope.push("rot_y", euler.1 as f64);
        scope.push("rot_z", euler.2 as f64);
        scope.push("scale_x", transform.scale.x as f64);
        scope.push("scale_y", transform.scale.y as f64);
        scope.push("scale_z", transform.scale.z as f64);
        scope.push("dt", delta_time as f64);
        scope.push("delta_time", delta_time as f64);
        scope.push("time", runtime.accumulated_time);
        scope.push("mouse_x", mouse_position.0);
        scope.push("mouse_y", mouse_position.1);
        scope.push("pressed_keys", pressed_keys.clone());
        scope.push("just_pressed_keys", just_pressed_keys.clone());
        scope.push_constant("entity_id", entity.id as i64);

        scope.push("spawn_cube_x", 0.0_f64);
        scope.push("spawn_cube_y", 0.0_f64);
        scope.push("spawn_cube_z", 0.0_f64);
        scope.push("do_spawn_cube", false);

        scope.push("spawn_sphere_x", 0.0_f64);
        scope.push("spawn_sphere_y", 0.0_f64);
        scope.push("spawn_sphere_z", 0.0_f64);
        scope.push("do_spawn_sphere", false);

        scope.push("do_despawn", false);

        let mut state_map = rhai::Map::new();
        for (key, value) in &runtime.game_state {
            state_map.insert(key.clone().into(), rhai::Dynamic::from(*value));
        }
        scope.push("state", state_map);

        let result = execute_script(runtime, &script, &mut scope);

        match result {
            Ok(()) => {
                let new_pos_x = scope
                    .get_value::<f64>("pos_x")
                    .unwrap_or(transform.translation.x as f64);
                let new_pos_y = scope
                    .get_value::<f64>("pos_y")
                    .unwrap_or(transform.translation.y as f64);
                let new_pos_z = scope
                    .get_value::<f64>("pos_z")
                    .unwrap_or(transform.translation.z as f64);

                let new_scale_x = scope
                    .get_value::<f64>("scale_x")
                    .unwrap_or(transform.scale.x as f64);
                let new_scale_y = scope
                    .get_value::<f64>("scale_y")
                    .unwrap_or(transform.scale.y as f64);
                let new_scale_z = scope
                    .get_value::<f64>("scale_z")
                    .unwrap_or(transform.scale.z as f64);

                let new_euler_x = scope.get_value::<f64>("rot_x").unwrap_or(euler.0 as f64);
                let new_euler_y = scope.get_value::<f64>("rot_y").unwrap_or(euler.1 as f64);
                let new_euler_z = scope.get_value::<f64>("rot_z").unwrap_or(euler.2 as f64);
                let new_rotation =
                    euler_to_quat(new_euler_x as f32, new_euler_y as f32, new_euler_z as f32);

                let do_spawn_cube = scope.get_value::<bool>("do_spawn_cube").unwrap_or(false);
                let do_spawn_sphere = scope.get_value::<bool>("do_spawn_sphere").unwrap_or(false);
                let do_despawn = scope.get_value::<bool>("do_despawn").unwrap_or(false);

                let mut pending_spawns = Vec::new();
                if do_spawn_cube {
                    let x = scope.get_value::<f64>("spawn_cube_x").unwrap_or(0.0);
                    let y = scope.get_value::<f64>("spawn_cube_y").unwrap_or(0.0);
                    let z = scope.get_value::<f64>("spawn_cube_z").unwrap_or(0.0);
                    pending_spawns.push((
                        "Cube".to_string(),
                        nalgebra_glm::vec3(x as f32, y as f32, z as f32),
                    ));
                }
                if do_spawn_sphere {
                    let x = scope.get_value::<f64>("spawn_sphere_x").unwrap_or(0.0);
                    let y = scope.get_value::<f64>("spawn_sphere_y").unwrap_or(0.0);
                    let z = scope.get_value::<f64>("spawn_sphere_z").unwrap_or(0.0);
                    pending_spawns.push((
                        "Sphere".to_string(),
                        nalgebra_glm::vec3(x as f32, y as f32, z as f32),
                    ));
                }

                if let Some(updated_state) = scope.get_value::<rhai::Map>("state") {
                    for (key, value) in updated_state {
                        if let Some(num) = value
                            .as_float()
                            .ok()
                            .or_else(|| value.as_int().ok().map(|i| i as f64))
                        {
                            runtime.game_state.insert(key.to_string(), num);
                        }
                    }
                }

                let pos_changed = (new_pos_x as f32 - transform.translation.x).abs() > f32::EPSILON
                    || (new_pos_y as f32 - transform.translation.y).abs() > f32::EPSILON
                    || (new_pos_z as f32 - transform.translation.z).abs() > f32::EPSILON;

                let scale_changed = (new_scale_x as f32 - transform.scale.x).abs() > f32::EPSILON
                    || (new_scale_y as f32 - transform.scale.y).abs() > f32::EPSILON
                    || (new_scale_z as f32 - transform.scale.z).abs() > f32::EPSILON;

                let rot_changed = (new_euler_x as f32 - euler.0).abs() > f32::EPSILON
                    || (new_euler_y as f32 - euler.1).abs() > f32::EPSILON
                    || (new_euler_z as f32 - euler.2).abs() > f32::EPSILON;

                commands.push((
                    entity,
                    ScriptResult {
                        new_position: if pos_changed {
                            Some(nalgebra_glm::vec3(
                                new_pos_x as f32,
                                new_pos_y as f32,
                                new_pos_z as f32,
                            ))
                        } else {
                            None
                        },
                        new_rotation: if rot_changed {
                            Some(new_rotation)
                        } else {
                            None
                        },
                        new_scale: if scale_changed {
                            Some(nalgebra_glm::vec3(
                                new_scale_x as f32,
                                new_scale_y as f32,
                                new_scale_z as f32,
                            ))
                        } else {
                            None
                        },
                        pending_spawns,
                        should_despawn: do_despawn,
                    },
                ));
            }
            Err(error) => {
                tracing::error!("Script error on entity {:?}: {}", entity, error);
            }
        }
    }

    for (entity, result) in commands {
        if result.should_despawn {
            world.queue_command(WorldCommand::DespawnRecursive { entity });
            runtime.remove_entity_scope(entity);
            continue;
        }

        if let Some(pos) = result.new_position {
            if let Some(transform) = world.get_local_transform_mut(entity) {
                transform.translation = pos;
            }
            world.mark_local_transform_dirty(entity);
        }

        if let Some(rot) = result.new_rotation {
            if let Some(transform) = world.get_local_transform_mut(entity) {
                transform.rotation = rot;
            }
            world.mark_local_transform_dirty(entity);
        }

        if let Some(scale) = result.new_scale {
            if let Some(transform) = world.get_local_transform_mut(entity) {
                transform.scale = scale;
            }
            world.mark_local_transform_dirty(entity);
        }

        for (mesh_type, position) in result.pending_spawns {
            match mesh_type.as_str() {
                "Cube" => {
                    spawn_cube_at(world, position);
                }
                "Sphere" => {
                    spawn_sphere_at(world, position);
                }
                _ => {
                    tracing::warn!("Unknown spawn type: {}", mesh_type);
                }
            }
        }
    }

    runtime.previous_keys.clear();
    for (key, state) in &world.resources.input.keyboard.keystates {
        if *state == winit::event::ElementState::Pressed {
            runtime.previous_keys.insert(*key);
        }
    }
}

struct ScriptResult {
    new_position: Option<nalgebra_glm::Vec3>,
    new_rotation: Option<nalgebra_glm::Quat>,
    new_scale: Option<nalgebra_glm::Vec3>,
    pending_spawns: Vec<(String, nalgebra_glm::Vec3)>,
    should_despawn: bool,
}

fn execute_script(
    runtime: &mut ScriptRuntime,
    script: &Script,
    scope: &mut Scope,
) -> Result<(), String> {
    let (key, source) = match &script.source {
        ScriptSource::File { path } => {
            #[cfg(not(target_arch = "wasm32"))]
            {
                runtime.track_file(path);
                let source = std::fs::read_to_string(path)
                    .map_err(|e| format!("Failed to read script file '{}': {}", path, e))?;
                (path.clone(), source)
            }
            #[cfg(target_arch = "wasm32")]
            {
                let _ = path;
                return Err("File-based scripts not supported on wasm".to_string());
            }
        }
        ScriptSource::Embedded { source } => {
            let key = format!(
                "embedded_{}",
                scope.get_value::<i64>("entity_id").unwrap_or(0)
            );
            (key, source.clone())
        }
    };

    let ast = runtime.compile_script(&key, &source)?.clone();

    runtime
        .engine
        .run_ast_with_scope(scope, &ast)
        .map_err(|e| format!("Runtime error: {}", e))?;

    Ok(())
}

fn build_pressed_keys(world: &World) -> rhai::Array {
    let keyboard = &world.resources.input.keyboard;
    let mut keys = Vec::new();

    for (key, state) in &keyboard.keystates {
        if *state == winit::event::ElementState::Pressed
            && let Some(name) = keycode_to_string(*key)
        {
            keys.push(rhai::Dynamic::from(name));
        }
    }

    keys
}

fn build_just_pressed_keys(world: &World, runtime: &ScriptRuntime) -> rhai::Array {
    let keyboard = &world.resources.input.keyboard;
    let mut keys = Vec::new();

    for (key, state) in &keyboard.keystates {
        if *state == winit::event::ElementState::Pressed
            && !runtime.previous_keys.contains(key)
            && let Some(name) = keycode_to_string(*key)
        {
            keys.push(rhai::Dynamic::from(name));
        }
    }

    keys
}

fn keycode_to_string(key: KeyCode) -> Option<String> {
    let name = match key {
        KeyCode::KeyA => "A",
        KeyCode::KeyB => "B",
        KeyCode::KeyC => "C",
        KeyCode::KeyD => "D",
        KeyCode::KeyE => "E",
        KeyCode::KeyF => "F",
        KeyCode::KeyG => "G",
        KeyCode::KeyH => "H",
        KeyCode::KeyI => "I",
        KeyCode::KeyJ => "J",
        KeyCode::KeyK => "K",
        KeyCode::KeyL => "L",
        KeyCode::KeyM => "M",
        KeyCode::KeyN => "N",
        KeyCode::KeyO => "O",
        KeyCode::KeyP => "P",
        KeyCode::KeyQ => "Q",
        KeyCode::KeyR => "R",
        KeyCode::KeyS => "S",
        KeyCode::KeyT => "T",
        KeyCode::KeyU => "U",
        KeyCode::KeyV => "V",
        KeyCode::KeyW => "W",
        KeyCode::KeyX => "X",
        KeyCode::KeyY => "Y",
        KeyCode::KeyZ => "Z",
        KeyCode::Digit0 => "0",
        KeyCode::Digit1 => "1",
        KeyCode::Digit2 => "2",
        KeyCode::Digit3 => "3",
        KeyCode::Digit4 => "4",
        KeyCode::Digit5 => "5",
        KeyCode::Digit6 => "6",
        KeyCode::Digit7 => "7",
        KeyCode::Digit8 => "8",
        KeyCode::Digit9 => "9",
        KeyCode::Space => "SPACE",
        KeyCode::Enter => "ENTER",
        KeyCode::Escape => "ESCAPE",
        KeyCode::ShiftLeft | KeyCode::ShiftRight => "SHIFT",
        KeyCode::ControlLeft | KeyCode::ControlRight => "CTRL",
        KeyCode::AltLeft | KeyCode::AltRight => "ALT",
        KeyCode::Tab => "TAB",
        KeyCode::Backspace => "BACKSPACE",
        KeyCode::ArrowUp => "UP",
        KeyCode::ArrowDown => "DOWN",
        KeyCode::ArrowLeft => "LEFT",
        KeyCode::ArrowRight => "RIGHT",
        _ => return None,
    };
    Some(name.to_string())
}

fn quat_to_euler(q: &nalgebra_glm::Quat) -> (f32, f32, f32) {
    let sinr_cosp = 2.0 * (q.w * q.i + q.j * q.k);
    let cosr_cosp = 1.0 - 2.0 * (q.i * q.i + q.j * q.j);
    let roll = sinr_cosp.atan2(cosr_cosp);

    let sinp = 2.0 * (q.w * q.j - q.k * q.i);
    let pitch = if sinp.abs() >= 1.0 {
        std::f32::consts::FRAC_PI_2.copysign(sinp)
    } else {
        sinp.asin()
    };

    let siny_cosp = 2.0 * (q.w * q.k + q.i * q.j);
    let cosy_cosp = 1.0 - 2.0 * (q.j * q.j + q.k * q.k);
    let yaw = siny_cosp.atan2(cosy_cosp);

    (roll, pitch, yaw)
}

fn euler_to_quat(roll: f32, pitch: f32, yaw: f32) -> nalgebra_glm::Quat {
    let cr = (roll * 0.5).cos();
    let sr = (roll * 0.5).sin();
    let cp = (pitch * 0.5).cos();
    let sp = (pitch * 0.5).sin();
    let cy = (yaw * 0.5).cos();
    let sy = (yaw * 0.5).sin();

    nalgebra_glm::quat(
        sr * cp * cy - cr * sp * sy,
        cr * sp * cy + sr * cp * sy,
        cr * cp * sy - sr * sp * cy,
        cr * cp * cy + sr * sp * sy,
    )
}
