pub use crate::ecs::camera::commands::*;
pub use crate::ecs::camera::queries::*;

use crate::ecs::camera::components::{compute_pan_orbit_transform, compute_third_person_transform};
#[cfg(feature = "gamepad")]
use crate::ecs::input::queries::query_active_gamepad;
#[cfg(feature = "physics")]
use crate::ecs::physics::resources::physics_world_query_pipeline;
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::world::{
    PAN_ORBIT_CAMERA, THIRD_PERSON_CAMERA, Vec2, Vec3, World,
    components::{Camera, Projection, Smoothing},
    resources::MouseState,
};
use crate::prelude::*;

fn retained_ui_wants_pointer(world: &World) -> bool {
    world.resources.retained_ui.enabled && ui_pointer_over_widget(world)
}

#[cfg(target_arch = "wasm32")]
pub fn sync_wasm_canvas_size(world: &mut World) {
    use winit::platform::web::WindowExtWebSys;
    if let Some(window_handle) = &world.resources.window.handle
        && let Some(canvas) = window_handle.canvas()
    {
        world.resources.window.cached_viewport_size = Some((canvas.width(), canvas.height()));
    }
}

pub fn fly_camera_system(world: &mut World) {
    if world.resources.retained_ui.enabled
        && (world
            .resources
            .retained_ui
            .interaction
            .active_entity
            .is_some()
            || world
                .resources
                .retained_ui
                .interaction
                .hovered_entity
                .is_some())
    {
        return;
    }
    look_camera_system(world);
    wasd_keyboard_controls_system(world);
    #[cfg(feature = "gamepad")]
    gamepad_fly_camera_system(world);
    touch_fly_camera_system(world);
}

/// Routes input to the right camera controller based on the active camera's
/// component set. Use this when an app has exactly one camera-controller mode
/// and wants the engine to pick the right system; for explicit per-mode
/// dispatch (e.g. an editor with an Orbit/Fly toggle), call the per-controller
/// systems directly.
pub fn camera_controllers_system(world: &mut World) {
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };
    if world
        .core
        .entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
    {
        pan_orbit_camera_system(world);
    } else {
        fly_camera_system(world);
    }
}

pub fn first_person_camera_look_system(world: &mut World) {
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    let smoothing = world
        .core
        .get_camera(camera_entity)
        .and_then(|camera| camera.smoothing);
    let mouse_sens_scale = smoothing.map(|s| s.mouse_sensitivity).unwrap_or(0.5);
    let gamepad_sens_scale = smoothing.map(|s| s.gamepad_sensitivity).unwrap_or(1.5);
    let mouse_sensitivity = 0.004 * mouse_sens_scale;
    let gamepad_sensitivity = (5.0 / 3.0) * gamepad_sens_scale;
    let pitch_limit = 85_f32.to_radians();

    let cursor_locked = world.resources.window.handle.as_ref().is_some_and(|_| true);

    let raw_delta = crate::ecs::input::access::mouse_for_active(world).raw_mouse_delta;
    let has_mouse_input = raw_delta.x.abs() > 0.0 || raw_delta.y.abs() > 0.0;

    #[cfg(feature = "gamepad")]
    let (gamepad_right_stick_x, gamepad_right_stick_y) = {
        let deadzone = 0.15_f32;
        if let Some(gamepad) = query_active_gamepad(world) {
            let raw_x = gamepad.value(gilrs::Axis::RightStickX);
            let raw_y = gamepad.value(gilrs::Axis::RightStickY);
            let magnitude = (raw_x * raw_x + raw_y * raw_y).sqrt();
            if magnitude > deadzone {
                let normalized = (magnitude - deadzone) / (1.0 - deadzone);
                (
                    raw_x * normalized / magnitude,
                    raw_y * normalized / magnitude,
                )
            } else {
                (0.0, 0.0)
            }
        } else {
            (0.0, 0.0)
        }
    };

    #[cfg(not(feature = "gamepad"))]
    let (gamepad_right_stick_x, gamepad_right_stick_y) = (0.0_f32, 0.0_f32);

    let has_gamepad_input = gamepad_right_stick_x.abs() > 0.0 || gamepad_right_stick_y.abs() > 0.0;

    if !has_mouse_input && !has_gamepad_input {
        return;
    }

    let dt = world.resources.window.timing.delta_time;

    let delta = if has_gamepad_input {
        Vec2::new(
            gamepad_right_stick_x * gamepad_sensitivity * dt,
            -gamepad_right_stick_y * gamepad_sensitivity * dt,
        )
    } else if has_mouse_input && cursor_locked {
        raw_delta * mouse_sensitivity
    } else {
        return;
    };

    let Some(camera_transform) = world.core.get_local_transform_mut(camera_entity) else {
        return;
    };

    let yaw = nalgebra_glm::quat_angle_axis(-delta.x, &Vec3::new(0.0, 1.0, 0.0));
    camera_transform.rotation = yaw * camera_transform.rotation;

    let forward =
        nalgebra_glm::quat_rotate_vec3(&camera_transform.rotation, &Vec3::new(0.0, 0.0, -1.0));
    let current_pitch = forward.y.asin();
    let new_pitch = current_pitch - delta.y;

    if new_pitch.abs() <= pitch_limit {
        let pitch = nalgebra_glm::quat_angle_axis(-delta.y, &Vec3::new(1.0, 0.0, 0.0));
        camera_transform.rotation *= pitch;
    }

    mark_local_transform_dirty(world, camera_entity);
}

fn look_camera_system(world: &mut World) {
    use std::sync::atomic::{AtomicBool, Ordering};
    static CAMERA_HAS_CURSOR: AtomicBool = AtomicBool::new(false);

    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    let delta_time = world.resources.window.timing.delta_time;

    let (right, up) = {
        let Some(local_transform) = world.core.get_local_transform(camera_entity) else {
            return;
        };
        (local_transform.right_vector(), local_transform.up_vector())
    };

    let right_clicked = world
        .resources
        .input
        .mouse
        .state
        .contains(MouseState::RIGHT_CLICKED);

    if right_clicked {
        let already_grabbed = CAMERA_HAS_CURSOR.load(Ordering::Relaxed);

        if !already_grabbed {
            if let Some(window_handle) = &world.resources.window.handle {
                if window_handle
                    .set_cursor_grab(winit::window::CursorGrabMode::Locked)
                    .is_err()
                {
                    let _ = window_handle.set_cursor_grab(winit::window::CursorGrabMode::Confined);
                }
                window_handle.set_cursor_visible(false);
            }
            CAMERA_HAS_CURSOR.store(true, Ordering::Relaxed);
        }

        let raw_delta = crate::ecs::input::access::mouse_for_active(world).raw_mouse_delta;

        let Some(camera) = world.core.get_camera_mut(camera_entity) else {
            return;
        };
        let Some(smoothing) = camera.smoothing.as_mut() else {
            return;
        };

        let smoothing_factor = if smoothing.mouse_smoothness > 0.0 {
            1.0 - smoothing.mouse_smoothness.powi(7).powf(delta_time)
        } else {
            1.0
        };
        smoothing.smoothed_mouse_delta = smoothing.smoothed_mouse_delta * (1.0 - smoothing_factor)
            + raw_delta * smoothing_factor;

        let pixels_to_radians = (std::f32::consts::PI / 1000.0) * smoothing.mouse_dpi_scale;
        let mut delta =
            smoothing.smoothed_mouse_delta * smoothing.mouse_sensitivity * pixels_to_radians;
        delta.x *= -1.0;
        delta.y *= -1.0;

        let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
            return;
        };

        let yaw = nalgebra_glm::quat_angle_axis(delta.x, &Vec3::y());
        local_transform.rotation = yaw * local_transform.rotation;

        let forward = local_transform.forward_vector();
        let current_pitch = forward.y.asin();

        let new_pitch = current_pitch + delta.y;
        if new_pitch.abs() <= 89_f32.to_radians() {
            let pitch = nalgebra_glm::quat_angle_axis(delta.y, &Vec3::x());
            local_transform.rotation *= pitch;
        }

        mark_local_transform_dirty(world, camera_entity);
    } else {
        if CAMERA_HAS_CURSOR.load(Ordering::Relaxed) {
            if let Some(window_handle) = &world.resources.window.handle {
                let _ = window_handle.set_cursor_grab(winit::window::CursorGrabMode::None);
                window_handle.set_cursor_visible(true);
            }
            CAMERA_HAS_CURSOR.store(false, Ordering::Relaxed);
        }

        if let Some(Camera {
            smoothing:
                Some(Smoothing {
                    smoothed_mouse_delta,
                    mouse_smoothness,
                    ..
                }),
            ..
        }) = world.core.get_camera_mut(camera_entity)
        {
            let decay_smoothness = (*mouse_smoothness * 0.5).max(0.01);
            let smoothing_factor = 1.0 - decay_smoothness.powi(7).powf(delta_time);
            *smoothed_mouse_delta =
                *smoothed_mouse_delta * (1.0 - smoothing_factor) + Vec2::zeros() * smoothing_factor;
        }
    }

    if world
        .resources
        .input
        .mouse
        .state
        .contains(MouseState::MIDDLE_CLICKED)
    {
        let mut delta = crate::ecs::input::access::mouse_for_active(world).position_delta
            * world.resources.window.timing.delta_time;
        delta.x *= -1.0;
        delta.y *= -1.0;

        let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
            return;
        };
        let translation_right = right * delta.x;
        let translation_up = up * delta.y;

        local_transform.translation += translation_right;
        local_transform.translation += translation_up;

        let changed = translation_right.magnitude() > 0.0 || translation_up.magnitude() > 0.0;
        if changed {
            mark_local_transform_dirty(world, camera_entity);
        }
    }
}

fn wasd_keyboard_controls_system(world: &mut World) {
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };
    let delta_time = world.resources.window.timing.delta_time;

    let (
        left_key_pressed,
        right_key_pressed,
        forward_key_pressed,
        backward_key_pressed,
        up_key_pressed,
        shift_pressed,
    ) = {
        let keyboard = crate::ecs::input::access::keyboard_for_active(world);
        (
            keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyA),
            keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyD),
            keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyW),
            keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyS),
            keyboard.is_key_pressed(winit::keyboard::KeyCode::Space),
            keyboard.is_key_pressed(winit::keyboard::KeyCode::ShiftLeft)
                || keyboard.is_key_pressed(winit::keyboard::KeyCode::ShiftRight),
        )
    };

    let base_speed = if shift_pressed { 60.0 } else { 20.0 };

    let mut target_movement = Vec3::zeros();

    if forward_key_pressed {
        target_movement.z += 1.0;
    }
    if backward_key_pressed {
        target_movement.z -= 1.0;
    }
    if left_key_pressed {
        target_movement.x -= 1.0;
    }
    if right_key_pressed {
        target_movement.x += 1.0;
    }
    if up_key_pressed {
        target_movement.y += 1.0;
    }

    if target_movement.magnitude() > 0.0 {
        target_movement = target_movement.normalize();
    }

    let Some(camera) = world.core.get_camera_mut(camera_entity) else {
        return;
    };
    let Some(smoothing) = camera.smoothing.as_mut() else {
        return;
    };

    let smoothing_factor = if smoothing.keyboard_smoothness > 0.0 {
        1.0 - smoothing.keyboard_smoothness.powi(7).powf(delta_time)
    } else {
        1.0
    };
    smoothing.smoothed_movement =
        smoothing.smoothed_movement * (1.0 - smoothing_factor) + target_movement * smoothing_factor;

    let movement = smoothing.smoothed_movement;

    let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
        return;
    };
    let forward = local_transform.forward_vector();
    let right = local_transform.right_vector();
    let up = local_transform.up_vector();

    let forward_translation = forward * movement.z * base_speed * delta_time;
    let right_translation = right * movement.x * base_speed * delta_time;
    let up_translation = up * movement.y * base_speed * delta_time;

    local_transform.translation += forward_translation;
    local_transform.translation += right_translation;
    local_transform.translation += up_translation;

    let changed = forward_translation.magnitude() > 0.0
        || right_translation.magnitude() > 0.0
        || up_translation.magnitude() > 0.0;

    if changed {
        mark_local_transform_dirty(world, camera_entity);
    }
}

fn pan_orbit_viewport_size(world: &World) -> Vec2 {
    if let Some(window_handle) = &world.resources.window.handle {
        let size = window_handle.inner_size();
        return Vec2::new((size.width as f32).max(1.0), (size.height as f32).max(1.0));
    }
    if let Some((width, height)) = world.resources.window.cached_viewport_size {
        return Vec2::new((width as f32).max(1.0), (height as f32).max(1.0));
    }
    Vec2::new(1920.0, 1080.0)
}

fn pan_orbit_camera_y_fov(world: &World, camera_entity: crate::ecs::world::Entity) -> f32 {
    world
        .core
        .get_camera(camera_entity)
        .and_then(|camera| match camera.projection {
            crate::ecs::camera::components::Projection::Perspective(perspective) => {
                Some(perspective.y_fov_rad)
            }
            _ => None,
        })
        .unwrap_or(45.0_f32.to_radians())
}

#[cfg(feature = "gamepad")]
fn apply_deadzone(value: f32, deadzone: f32) -> f32 {
    if value.abs() > deadzone {
        let sign = value.signum();
        let compensated = (value.abs() - deadzone) / (1.0 - deadzone);
        sign * compensated
    } else {
        0.0
    }
}

#[cfg(feature = "gamepad")]
fn gamepad_fly_camera_system(world: &mut World) {
    if world.resources.input.gamepad.gilrs.is_none() {
        return;
    }

    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    let delta_time = world.resources.window.timing.delta_time;
    let base_speed = 20.0;

    let (
        left_trigger2_pressed,
        right_trigger2_pressed,
        left_stick_x_axis_data,
        left_stick_y_axis_data,
        right_stick_x_axis_data,
        right_stick_y_axis_data,
        select_pressed,
    ) = {
        let Some(gamepad) = query_active_gamepad(world) else {
            return;
        };
        (
            gamepad.is_pressed(gilrs::Button::LeftTrigger2),
            gamepad.is_pressed(gilrs::Button::RightTrigger2),
            gamepad.axis_data(gilrs::Axis::LeftStickX).cloned(),
            gamepad.axis_data(gilrs::Axis::LeftStickY).cloned(),
            gamepad.axis_data(gilrs::Axis::RightStickX).cloned(),
            gamepad.axis_data(gilrs::Axis::RightStickY).cloned(),
            gamepad.is_pressed(gilrs::Button::Select),
        )
    };

    if select_pressed {
        world.resources.window.should_exit = true;
    }

    let Some(camera) = world.core.get_camera_mut(camera_entity) else {
        return;
    };
    let Some(smoothing) = camera.smoothing.as_mut() else {
        return;
    };

    let deadzone = smoothing.gamepad_deadzone;

    let mut target_rotation = Vec2::zeros();
    if let Some(axis_data) = right_stick_x_axis_data {
        target_rotation.x = apply_deadzone(axis_data.value(), deadzone);
    }
    if let Some(axis_data) = right_stick_y_axis_data {
        target_rotation.y = apply_deadzone(axis_data.value(), deadzone);
    }

    let smoothing_factor = if smoothing.gamepad_smoothness > 0.0 {
        1.0 - smoothing.gamepad_smoothness.powi(7).powf(delta_time)
    } else {
        1.0
    };
    smoothing.smoothed_gamepad_input = smoothing.smoothed_gamepad_input * (1.0 - smoothing_factor)
        + target_rotation * smoothing_factor;

    let rotation_speed = smoothing.smoothed_gamepad_input * smoothing.gamepad_sensitivity * 2.0;

    let Some(transform) = world.core.get_local_transform_mut(camera_entity) else {
        return;
    };

    let forward = transform.forward_vector();
    let right = transform.right_vector();
    let up = transform.up_vector();

    let mut changed = false;

    #[cfg(target_arch = "wasm32")]
    let (up_trigger_pressed, down_trigger_pressed) =
        (left_trigger2_pressed, right_trigger2_pressed);
    #[cfg(not(target_arch = "wasm32"))]
    let (up_trigger_pressed, down_trigger_pressed) =
        (right_trigger2_pressed, left_trigger2_pressed);

    if up_trigger_pressed {
        let translation = up * base_speed * delta_time;
        changed |= translation.magnitude() > 0.0;
        transform.translation += translation;
    }

    if down_trigger_pressed {
        let translation = up * base_speed * delta_time;
        changed |= translation.magnitude() > 0.0;
        transform.translation -= translation;
    }

    if let Some(axis_data) = left_stick_x_axis_data {
        let compensated_value = apply_deadzone(axis_data.value(), deadzone);
        let translation = right * compensated_value * base_speed * delta_time;
        changed |= translation.magnitude() > 0.0;
        transform.translation += translation;
    }

    if let Some(axis_data) = left_stick_y_axis_data {
        let compensated_value = apply_deadzone(axis_data.value(), deadzone);
        let translation = forward * compensated_value * base_speed * delta_time;
        changed |= translation.magnitude() > 0.0;
        transform.translation += translation;
    }

    if rotation_speed.x.abs() > 0.001 {
        let yaw = nalgebra_glm::quat_angle_axis(-(rotation_speed.x * delta_time), &Vec3::y());
        transform.rotation = yaw * transform.rotation;
        changed = true;
    }

    if rotation_speed.y.abs() > 0.001 {
        let forward = transform.forward_vector();
        let current_pitch = forward.y.asin();
        let new_pitch = current_pitch + rotation_speed.y * delta_time;
        if new_pitch.abs() <= 89_f32.to_radians() {
            let pitch = nalgebra_glm::quat_angle_axis(rotation_speed.y * delta_time, &Vec3::x());
            transform.rotation *= pitch;
        }
        changed = true;
    }

    if changed {
        mark_local_transform_dirty(world, camera_entity);
    }
}

fn touch_fly_camera_system(world: &mut World) {
    use crate::ecs::input::resources::TouchGesture;

    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    if retained_ui_wants_pointer(world) {
        return;
    }

    let gesture = world.resources.input.touch.gesture;

    let delta_time = world.resources.window.timing.delta_time;
    let base_speed = 20.0;

    match gesture {
        TouchGesture::SingleDrag { delta } => {
            let pixels_to_radians = std::f32::consts::PI / 500.0;
            let rotation_delta = delta * pixels_to_radians;

            let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
                return;
            };

            let yaw = nalgebra_glm::quat_angle_axis(-rotation_delta.x, &Vec3::y());
            local_transform.rotation = yaw * local_transform.rotation;

            let forward = local_transform.forward_vector();
            let current_pitch = forward.y.asin();

            let new_pitch = current_pitch - rotation_delta.y;
            if new_pitch.abs() <= 89_f32.to_radians() {
                let pitch = nalgebra_glm::quat_angle_axis(-rotation_delta.y, &Vec3::x());
                local_transform.rotation *= pitch;
            }

            mark_local_transform_dirty(world, camera_entity);
        }
        TouchGesture::TwoFingerDrag { delta } => {
            let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
                return;
            };

            let right = local_transform.right_vector();
            let up = local_transform.up_vector();

            let pan_scale = delta_time * base_speed * 0.1;
            let translation_right = right * -delta.x * pan_scale;
            let translation_up = up * delta.y * pan_scale;

            local_transform.translation += translation_right;
            local_transform.translation += translation_up;

            let changed = translation_right.magnitude() > 0.0 || translation_up.magnitude() > 0.0;
            if changed {
                mark_local_transform_dirty(world, camera_entity);
            }
        }
        TouchGesture::Pinch { delta, .. } => {
            let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
                return;
            };

            let forward = local_transform.forward_vector();
            let move_speed = delta * delta_time * base_speed * 0.05;
            local_transform.translation += forward * move_speed;

            if move_speed.abs() > 0.0 {
                mark_local_transform_dirty(world, camera_entity);
            }
        }
        TouchGesture::None => {}
    }
}

pub fn pan_orbit_camera_system(world: &mut World) {
    pan_orbit_mouse_input(world);
    #[cfg(feature = "gamepad")]
    pan_orbit_gamepad_input(world);
    pan_orbit_touch_input(world);
    pan_orbit_update_transform(world);
}

fn pan_orbit_mouse_input(world: &mut World) {
    use crate::ecs::camera::components::{PanOrbitButton, PanOrbitModifier};

    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    if !world
        .core
        .entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
    {
        return;
    }

    let mouse_in_viewport =
        if let Some(viewport_rect) = &world.resources.window.active_viewport_rect {
            let mouse_pos = crate::ecs::input::access::mouse_for_active(world).position;
            viewport_rect.contains(mouse_pos)
        } else {
            true
        };

    if !mouse_in_viewport {
        return;
    }

    if retained_ui_wants_pointer(world) && world.resources.window.active_viewport_rect.is_none() {
        return;
    }

    if world.resources.user_interface.hud_wants_pointer {
        return;
    }

    if world.resources.retained_ui.enabled
        && (world
            .resources
            .retained_ui
            .interaction
            .active_entity
            .is_some()
            || world
                .resources
                .retained_ui
                .interaction
                .hovered_entity
                .is_some())
    {
        return;
    }

    let mouse_state = crate::ecs::input::access::mouse_for_active(world).state;
    let drag_just_started = mouse_state.contains(MouseState::LEFT_JUST_PRESSED)
        || mouse_state.contains(MouseState::RIGHT_JUST_PRESSED)
        || mouse_state.contains(MouseState::MIDDLE_JUST_PRESSED);
    let position_delta = if drag_just_started {
        Vec2::new(0.0, 0.0)
    } else {
        crate::ecs::input::access::mouse_for_active(world).position_delta
    };
    let wheel_delta = crate::ecs::input::access::mouse_for_active(world).wheel_delta;

    let viewport_size = pan_orbit_viewport_size(world);
    let camera_y_fov = pan_orbit_camera_y_fov(world, camera_entity);

    let left_clicked = mouse_state.contains(MouseState::LEFT_CLICKED);
    let right_clicked = mouse_state.contains(MouseState::RIGHT_CLICKED);
    let middle_clicked = mouse_state.contains(MouseState::MIDDLE_CLICKED);
    let scrolled = mouse_state.contains(MouseState::SCROLLED);

    let (shift_pressed, ctrl_pressed, alt_pressed) = {
        let keyboard = crate::ecs::input::access::keyboard_for_active(world);
        (
            keyboard.is_key_pressed(winit::keyboard::KeyCode::ShiftLeft)
                || keyboard.is_key_pressed(winit::keyboard::KeyCode::ShiftRight),
            keyboard.is_key_pressed(winit::keyboard::KeyCode::ControlLeft)
                || keyboard.is_key_pressed(winit::keyboard::KeyCode::ControlRight),
            keyboard.is_key_pressed(winit::keyboard::KeyCode::AltLeft)
                || keyboard.is_key_pressed(winit::keyboard::KeyCode::AltRight),
        )
    };

    let is_button_pressed = |button: PanOrbitButton| -> bool {
        match button {
            PanOrbitButton::Left => left_clicked,
            PanOrbitButton::Right => right_clicked,
            PanOrbitButton::Middle => middle_clicked,
        }
    };

    let is_modifier_satisfied = |modifier: Option<PanOrbitModifier>| -> bool {
        match modifier {
            None => true,
            Some(PanOrbitModifier::Shift) => shift_pressed,
            Some(PanOrbitModifier::Control) => ctrl_pressed,
            Some(PanOrbitModifier::Alt) => alt_pressed,
        }
    };

    let (bindings, prior_pan_enabled) = match world.core.get_pan_orbit_camera(camera_entity) {
        Some(pan_orbit) => (pan_orbit.bindings, pan_orbit.enabled),
        None => return,
    };

    if !prior_pan_enabled {
        return;
    }

    let orbit_modifier_active = is_modifier_satisfied(bindings.orbit_modifier);
    let pan_modifier_active = is_modifier_satisfied(bindings.pan_modifier);

    let orbit_button_pressed = is_button_pressed(bindings.orbit);
    let pan_button_pressed = is_button_pressed(bindings.pan);

    let wants_zoom_drag = ctrl_pressed && middle_clicked;
    let wants_orbit = orbit_modifier_active
        && orbit_button_pressed
        && !wants_zoom_drag
        && !(bindings.pan_modifier.is_some() && pan_modifier_active);
    let wants_pan = pan_modifier_active
        && pan_button_pressed
        && !wants_zoom_drag
        && !(bindings.orbit_modifier.is_some() && orbit_modifier_active);

    let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera_entity) else {
        return;
    };

    if wants_zoom_drag {
        let zoom_basis = if position_delta.y < 0.0 {
            pan_orbit
                .target_radius
                .max(pan_orbit.pan_distance.unwrap_or(pan_orbit.target_radius))
        } else {
            pan_orbit.target_radius
        };
        let zoom_delta = -position_delta.y * zoom_basis * 0.01 * pan_orbit.sensitivity.zoom;

        pan_orbit.target_radius += zoom_delta;
        pan_orbit.target_radius = pan_orbit.target_radius.max(pan_orbit.limits.zoom_lower);
        if let Some(upper) = pan_orbit.limits.zoom_upper {
            pan_orbit.target_radius = pan_orbit.target_radius.min(upper);
        }
    }

    if wants_orbit {
        let delta_x = (position_delta.x / viewport_size.x) * std::f32::consts::PI * 2.0;
        let delta_y = (position_delta.y / viewport_size.y) * std::f32::consts::PI;

        let delta_x = if pan_orbit.is_upside_down {
            -delta_x
        } else {
            delta_x
        };

        pan_orbit.target_yaw -= delta_x * pan_orbit.sensitivity.orbit;
        pan_orbit.target_pitch += delta_y * pan_orbit.sensitivity.orbit;

        if pan_orbit.limits.allow_upside_down {
            pan_orbit.target_pitch %= std::f32::consts::PI * 2.0;
        } else {
            pan_orbit.target_pitch = pan_orbit
                .target_pitch
                .clamp(pan_orbit.limits.pitch_lower, pan_orbit.limits.pitch_upper);
        }
    }

    if wants_pan {
        let (_, rotation) = compute_pan_orbit_transform(
            pan_orbit.target_focus,
            pan_orbit.target_yaw,
            pan_orbit.target_pitch,
            pan_orbit.target_radius,
        );

        let right = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::x());
        let up = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::y());

        let pan_radius_basis = pan_orbit.pan_distance.unwrap_or(pan_orbit.target_radius);
        let world_per_pixel =
            (pan_radius_basis * 2.0 * (camera_y_fov * 0.5).tan()) / viewport_size.y;
        let pan_scale = world_per_pixel * pan_orbit.sensitivity.pan;
        let pan_right = right * -position_delta.x * pan_scale;
        let pan_up = up * position_delta.y * pan_scale;

        pan_orbit.target_focus += pan_right + pan_up;
    }

    if scrolled {
        let zoom_basis = if wheel_delta.y < 0.0 {
            pan_orbit
                .target_radius
                .max(pan_orbit.pan_distance.unwrap_or(pan_orbit.target_radius))
        } else {
            pan_orbit.target_radius
        };
        let zoom_delta = -wheel_delta.y * zoom_basis * 0.2 * pan_orbit.sensitivity.zoom;

        pan_orbit.target_radius += zoom_delta;
        pan_orbit.target_radius = pan_orbit.target_radius.max(pan_orbit.limits.zoom_lower);
        if let Some(upper) = pan_orbit.limits.zoom_upper {
            pan_orbit.target_radius = pan_orbit.target_radius.min(upper);
        }
    }

    if !orbit_button_pressed {
        pan_orbit.is_upside_down = pan_orbit.target_pitch.abs() > std::f32::consts::FRAC_PI_2;
    }
}

#[cfg(feature = "gamepad")]
fn pan_orbit_gamepad_input(world: &mut World) {
    if world.resources.input.gamepad.gilrs.is_none() {
        return;
    }

    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    if !world
        .core
        .entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
    {
        return;
    }

    let delta_time = world.resources.window.timing.delta_time;

    let (left_stick_x, left_stick_y, right_stick_x, right_stick_y, left_trigger, right_trigger) = {
        let Some(gamepad) = query_active_gamepad(world) else {
            return;
        };
        (
            gamepad
                .axis_data(gilrs::Axis::LeftStickX)
                .map(|a| a.value())
                .unwrap_or(0.0),
            gamepad
                .axis_data(gilrs::Axis::LeftStickY)
                .map(|a| a.value())
                .unwrap_or(0.0),
            gamepad
                .axis_data(gilrs::Axis::RightStickX)
                .map(|a| a.value())
                .unwrap_or(0.0),
            gamepad
                .axis_data(gilrs::Axis::RightStickY)
                .map(|a| a.value())
                .unwrap_or(0.0),
            if gamepad.is_pressed(gilrs::Button::LeftTrigger2) {
                1.0
            } else {
                0.0
            },
            if gamepad.is_pressed(gilrs::Button::RightTrigger2) {
                1.0
            } else {
                0.0
            },
        )
    };

    let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera_entity) else {
        return;
    };

    if !pan_orbit.enabled {
        return;
    }

    let deadzone = pan_orbit.limits.gamepad_deadzone;

    let target_orbit = Vec2::new(
        apply_deadzone(right_stick_x, deadzone),
        apply_deadzone(right_stick_y, deadzone),
    );
    let target_pan = Vec2::new(
        apply_deadzone(left_stick_x, deadzone),
        apply_deadzone(left_stick_y, deadzone),
    );

    let smoothing_factor = if pan_orbit.smoothness.gamepad > 0.0 {
        1.0 - pan_orbit.smoothness.gamepad.powi(7).powf(delta_time)
    } else {
        1.0
    };

    pan_orbit.smoothed_gamepad_orbit = pan_orbit.smoothed_gamepad_orbit * (1.0 - smoothing_factor)
        + target_orbit * smoothing_factor;
    pan_orbit.smoothed_gamepad_pan =
        pan_orbit.smoothed_gamepad_pan * (1.0 - smoothing_factor) + target_pan * smoothing_factor;

    let orbit_input = pan_orbit.smoothed_gamepad_orbit;
    let pan_input = pan_orbit.smoothed_gamepad_pan;

    if orbit_input.magnitude() > 0.001 {
        pan_orbit.target_yaw -= orbit_input.x * pan_orbit.sensitivity.gamepad_orbit * delta_time;
        pan_orbit.target_pitch += orbit_input.y * pan_orbit.sensitivity.gamepad_orbit * delta_time;

        pan_orbit.target_pitch = pan_orbit
            .target_pitch
            .clamp(pan_orbit.limits.pitch_lower, pan_orbit.limits.pitch_upper);
    }

    if pan_input.magnitude() > 0.001 {
        let (_, rotation) = compute_pan_orbit_transform(
            pan_orbit.target_focus,
            pan_orbit.target_yaw,
            pan_orbit.target_pitch,
            pan_orbit.target_radius,
        );

        let right = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::x());
        let forward_flat = {
            let forward = nalgebra_glm::quat_rotate_vec3(&rotation, &-Vec3::z());
            Vec3::new(forward.x, 0.0, forward.z).normalize()
        };

        let pan_scale =
            pan_orbit.target_radius * pan_orbit.sensitivity.gamepad_pan * delta_time * 0.1;
        pan_orbit.target_focus += right * pan_input.x * pan_scale;
        pan_orbit.target_focus += forward_flat * pan_input.y * pan_scale;
    }

    let zoom_input: f32 = right_trigger - left_trigger;
    if zoom_input.abs() > 0.1 {
        let zoom_delta = zoom_input
            * pan_orbit.target_radius
            * pan_orbit.sensitivity.gamepad_zoom
            * delta_time
            * 0.1;

        pan_orbit.target_radius += zoom_delta;
        pan_orbit.target_radius = pan_orbit.target_radius.max(pan_orbit.limits.zoom_lower);
        if let Some(upper) = pan_orbit.limits.zoom_upper {
            pan_orbit.target_radius = pan_orbit.target_radius.min(upper);
        }
    }
}

fn pan_orbit_touch_input(world: &mut World) {
    use crate::ecs::input::resources::TouchGesture;

    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    if !world
        .core
        .entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
    {
        return;
    }

    if retained_ui_wants_pointer(world) {
        return;
    }

    if world.resources.user_interface.hud_wants_pointer {
        return;
    }

    let gesture = world.resources.input.touch.gesture;

    let viewport_size = pan_orbit_viewport_size(world);
    let camera_y_fov = pan_orbit_camera_y_fov(world, camera_entity);

    let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera_entity) else {
        return;
    };

    if !pan_orbit.enabled {
        return;
    }

    match gesture {
        TouchGesture::SingleDrag { delta } => {
            let delta_x = delta.x / viewport_size.x * std::f32::consts::PI * 2.0;
            let delta_y = delta.y / viewport_size.y * std::f32::consts::PI;

            pan_orbit.target_yaw -= delta_x * pan_orbit.sensitivity.orbit;
            pan_orbit.target_pitch += delta_y * pan_orbit.sensitivity.orbit;

            pan_orbit.target_pitch = pan_orbit
                .target_pitch
                .clamp(pan_orbit.limits.pitch_lower, pan_orbit.limits.pitch_upper);
        }
        TouchGesture::TwoFingerDrag { delta } => {
            let (_, rotation) = compute_pan_orbit_transform(
                pan_orbit.target_focus,
                pan_orbit.target_yaw,
                pan_orbit.target_pitch,
                pan_orbit.target_radius,
            );

            let right = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::x());
            let up = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::y());

            let world_per_pixel =
                (pan_orbit.target_radius * 2.0 * (camera_y_fov * 0.5).tan()) / viewport_size.y;
            let pan_scale = world_per_pixel * pan_orbit.sensitivity.pan;
            let pan_right = right * -delta.x * pan_scale;
            let pan_up = up * delta.y * pan_scale;

            pan_orbit.target_focus += pan_right + pan_up;
        }
        TouchGesture::Pinch { delta, .. } => {
            let zoom_delta = -delta * pan_orbit.target_radius * 0.005 * pan_orbit.sensitivity.zoom;

            pan_orbit.target_radius += zoom_delta;
            pan_orbit.target_radius = pan_orbit.target_radius.max(pan_orbit.limits.zoom_lower);
            if let Some(upper) = pan_orbit.limits.zoom_upper {
                pan_orbit.target_radius = pan_orbit.target_radius.min(upper);
            }
        }
        TouchGesture::None => {}
    }
}

pub fn pan_orbit_update_transform(world: &mut World) {
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    if !world
        .core
        .entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
    {
        return;
    }

    let delta_time = world.resources.window.timing.delta_time;

    let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera_entity) else {
        return;
    };

    if !pan_orbit.enabled {
        return;
    }

    let orbit_smoothness = pan_orbit.smoothness.orbit;
    let pan_smoothness = pan_orbit.smoothness.pan;
    let zoom_smoothness = pan_orbit.smoothness.zoom;

    pan_orbit.yaw = lerp_and_snap_f32(
        pan_orbit.yaw,
        pan_orbit.target_yaw,
        orbit_smoothness,
        delta_time,
    );
    pan_orbit.pitch = lerp_and_snap_f32(
        pan_orbit.pitch,
        pan_orbit.target_pitch,
        orbit_smoothness,
        delta_time,
    );
    pan_orbit.radius = lerp_and_snap_f32(
        pan_orbit.radius,
        pan_orbit.target_radius,
        zoom_smoothness,
        delta_time,
    );
    pan_orbit.focus = lerp_and_snap_vec3(
        pan_orbit.focus,
        pan_orbit.target_focus,
        pan_smoothness,
        delta_time,
    );

    let yaw = pan_orbit.yaw;
    let pitch = pan_orbit.pitch;
    let radius = pan_orbit.radius;
    let focus = pan_orbit.focus;

    let (position, rotation) = compute_pan_orbit_transform(focus, yaw, pitch, radius);

    let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
        return;
    };

    local_transform.rotation = rotation;
    local_transform.translation = position;

    mark_local_transform_dirty(world, camera_entity);
}

const SNAP_EPSILON: f32 = 0.001;

fn lerp_and_snap_f32(from: f32, to: f32, smoothness: f32, delta_time: f32) -> f32 {
    let t = smoothness.powi(7);
    let result = from + (to - from) * (1.0 - t.powf(delta_time));
    if smoothness < 1.0 && (result - to).abs() < SNAP_EPSILON {
        to
    } else {
        result
    }
}

fn lerp_and_snap_vec3(from: Vec3, to: Vec3, smoothness: f32, delta_time: f32) -> Vec3 {
    let t = smoothness.powi(7);
    let result = from + (to - from) * (1.0 - t.powf(delta_time));
    if smoothness < 1.0 && (result - to).magnitude() < SNAP_EPSILON {
        to
    } else {
        result
    }
}

pub fn ortho_camera_system(world: &mut World) {
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    let is_ortho = world
        .core
        .get_camera(camera_entity)
        .is_some_and(|camera| matches!(camera.projection, Projection::Orthographic(_)));

    if !is_ortho {
        return;
    }

    let delta_time = world.resources.window.timing.delta_time;

    if let Some(window_handle) = &world.resources.window.handle {
        let size = window_handle.inner_size();
        let width = size.width as f32;
        let height = size.height as f32;
        if let Some(camera) = world.core.get_camera_mut(camera_entity)
            && let Projection::Orthographic(ref mut ortho) = camera.projection
        {
            ortho.y_mag = ortho.x_mag * (height / width);
        }
    }

    let (x_mag, viewport_width) = {
        let camera = world.core.get_camera(camera_entity);
        let x_mag = camera
            .and_then(|camera| match camera.projection {
                Projection::Orthographic(ortho) => Some(ortho.x_mag),
                _ => None,
            })
            .unwrap_or(960.0);
        let viewport_width = world
            .resources
            .window
            .handle
            .as_ref()
            .map(|handle| handle.inner_size().width as f32)
            .unwrap_or(1920.0);
        (x_mag, viewport_width)
    };
    let zoom = viewport_width / (2.0 * x_mag);

    let keyboard = crate::ecs::input::access::keyboard_for_active(world);
    let left = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyA)
        || keyboard.is_key_pressed(winit::keyboard::KeyCode::ArrowLeft);
    let right = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyD)
        || keyboard.is_key_pressed(winit::keyboard::KeyCode::ArrowRight);
    let up = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyW)
        || keyboard.is_key_pressed(winit::keyboard::KeyCode::ArrowUp);
    let down = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyS)
        || keyboard.is_key_pressed(winit::keyboard::KeyCode::ArrowDown);
    let rotate_left = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyQ);
    let rotate_right = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyE);

    let wheel_delta = crate::ecs::input::access::mouse_for_active(world)
        .wheel_delta
        .y;

    let pan_speed = 400.0 / zoom;
    let mut movement = Vec2::new(0.0, 0.0);
    if left {
        movement.x -= pan_speed * delta_time;
    }
    if right {
        movement.x += pan_speed * delta_time;
    }
    if up {
        movement.y += pan_speed * delta_time;
    }
    if down {
        movement.y -= pan_speed * delta_time;
    }

    let mut changed = false;

    if (movement.x != 0.0 || movement.y != 0.0)
        && let Some(transform) = world.core.get_local_transform_mut(camera_entity)
    {
        let right_vec = transform.right_vector();
        let up_vec = transform.up_vector();
        transform.translation += right_vec * movement.x + up_vec * movement.y;
        changed = true;
    }

    if rotate_left || rotate_right {
        let rotation_speed = 2.0 * delta_time;
        if let Some(transform) = world.core.get_local_transform_mut(camera_entity) {
            if rotate_left {
                let rotation = nalgebra_glm::quat_angle_axis(-rotation_speed, &Vec3::z());
                transform.rotation = rotation * transform.rotation;
            }
            if rotate_right {
                let rotation = nalgebra_glm::quat_angle_axis(rotation_speed, &Vec3::z());
                transform.rotation = rotation * transform.rotation;
            }
            changed = true;
        }
    }

    if wheel_delta != 0.0 {
        if retained_ui_wants_pointer(world) {
            return;
        }

        if let Some(camera) = world.core.get_camera_mut(camera_entity)
            && let Projection::Orthographic(ref mut ortho) = camera.projection
        {
            let zoom_factor = 1.0 / (1.0 + wheel_delta * 0.1);
            ortho.x_mag *= zoom_factor;

            let min_x_mag = viewport_width / (2.0 * 10.0);
            let max_x_mag = viewport_width / (2.0 * 0.1);
            ortho.x_mag = ortho.x_mag.clamp(min_x_mag, max_x_mag);
            changed = true;
        }
    }

    if changed {
        mark_local_transform_dirty(world, camera_entity);
    }
}

pub fn third_person_camera_system(world: &mut World) {
    third_person_mouse_input(world);
    third_person_update_transform(world);
}

fn third_person_mouse_input(world: &mut World) {
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    if !world
        .core
        .entity_has_components(camera_entity, THIRD_PERSON_CAMERA)
    {
        return;
    };

    if retained_ui_wants_pointer(world) {
        return;
    }

    let mouse_state = crate::ecs::input::access::mouse_for_active(world).state;
    let position_delta = crate::ecs::input::access::mouse_for_active(world).position_delta;
    let wheel_delta = crate::ecs::input::access::mouse_for_active(world).wheel_delta;

    let window_size = if let Some(window_handle) = &world.resources.window.handle {
        let size = window_handle.inner_size();
        Vec2::new(size.width as f32, size.height as f32)
    } else {
        Vec2::new(1920.0, 1080.0)
    };

    let right_clicked = mouse_state.contains(MouseState::RIGHT_CLICKED);
    let scrolled = mouse_state.contains(MouseState::SCROLLED);

    let Some(camera) = world.core.get_third_person_camera_mut(camera_entity) else {
        return;
    };

    if right_clicked {
        let delta_x = (position_delta.x / window_size.x) * std::f32::consts::PI * 2.0;
        let delta_y = (position_delta.y / window_size.y) * std::f32::consts::PI;

        camera.target_yaw -= delta_x * camera.orbit_sensitivity;
        camera.target_pitch = (camera.target_pitch + delta_y * camera.orbit_sensitivity)
            .clamp(camera.pitch_lower_limit, camera.pitch_upper_limit);
    }

    if scrolled {
        let zoom_delta = -wheel_delta.y * camera.target_distance * 0.2 * camera.zoom_sensitivity;
        camera.target_distance =
            (camera.target_distance + zoom_delta).clamp(camera.distance_min, camera.distance_max);
    }
}

fn third_person_update_transform(world: &mut World) {
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };

    if !world
        .core
        .entity_has_components(camera_entity, THIRD_PERSON_CAMERA)
    {
        return;
    };

    let delta_time = world.resources.window.timing.delta_time;

    let (follow_target, height_offset) = {
        let Some(camera) = world.core.get_third_person_camera(camera_entity) else {
            return;
        };
        (camera.follow_target, camera.height_offset)
    };

    let target_position = follow_target
        .and_then(|target| world.core.get_local_transform(target))
        .map(|transform| transform.translation + Vec3::new(0.0, height_offset, 0.0))
        .unwrap_or_else(|| Vec3::new(0.0, height_offset, 0.0));

    let (focus, yaw, pitch, distance, _collision_enabled, _collision_radius) = {
        let Some(camera) = world.core.get_third_person_camera_mut(camera_entity) else {
            return;
        };

        let orbit_smoothness = camera.orbit_smoothness;
        let zoom_smoothness = camera.zoom_smoothness;
        let follow_smoothness = camera.follow_smoothness;

        camera.yaw = lerp_and_snap_f32(camera.yaw, camera.target_yaw, orbit_smoothness, delta_time);
        camera.pitch = lerp_and_snap_f32(
            camera.pitch,
            camera.target_pitch,
            orbit_smoothness,
            delta_time,
        );
        camera.distance = lerp_and_snap_f32(
            camera.distance,
            camera.target_distance,
            zoom_smoothness,
            delta_time,
        );
        camera.current_focus = lerp_and_snap_vec3(
            camera.current_focus,
            target_position,
            follow_smoothness,
            delta_time,
        );

        (
            camera.current_focus,
            camera.yaw,
            camera.pitch,
            camera.distance,
            camera.collision_enabled,
            camera.collision_radius,
        )
    };

    #[cfg(feature = "physics")]
    let distance = if _collision_enabled {
        let (ideal_position, _) = compute_third_person_transform(focus, yaw, pitch, distance);
        let direction = ideal_position - focus;
        let ray_length = nalgebra_glm::length(&direction);

        if ray_length > 0.001 {
            let ray_dir = direction / ray_length;
            let ray = rapier3d::prelude::Ray::new(
                rapier3d::na::Point3::new(focus.x, focus.y, focus.z),
                rapier3d::na::Vector3::new(ray_dir.x, ray_dir.y, ray_dir.z),
            );

            let query_pipeline = physics_world_query_pipeline(&world.resources.physics);
            if let Some((_handle, toi)) = query_pipeline.cast_ray(&ray, ray_length, true) {
                let safe_distance = (toi - _collision_radius).max(0.5);
                if safe_distance < distance {
                    safe_distance
                } else {
                    distance
                }
            } else {
                distance
            }
        } else {
            distance
        }
    } else {
        distance
    };

    let (position, rotation) = compute_third_person_transform(focus, yaw, pitch, distance);

    let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
        return;
    };

    local_transform.translation = position;
    local_transform.rotation = rotation;

    mark_local_transform_dirty(world, camera_entity);
}
