//! Reusable fly-camera controller.
//!
//! Lifted from the duplicated WASD + mouse-look code that every example used
//! to repeat. Add [`CameraPlugin`] to your app and tag your camera entity with
//! a [`FlyCam`] component.
//!
//! ```rust,ignore
//! use bevy::prelude::*;
//! use bevy_a5::camera::{CameraPlugin, FlyCam};
//!
//! App::new()
//!     .add_plugins((DefaultPlugins, CameraPlugin))
//!     .add_systems(Startup, |mut commands: Commands| {
//!         commands.spawn((
//!             Camera3d::default(),
//!             FlyCam::default(),
//!             Transform::from_xyz(0.0, 5.0, 20.0).looking_at(Vec3::ZERO, Vec3::Y),
//!         ));
//!     })
//!     .run();
//! ```

use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_input::mouse::AccumulatedMouseMotion;
use bevy_input::prelude::*;
use bevy_math::Vec3;
use bevy_reflect::Reflect;
use bevy_time::Time;
use bevy_transform::components::Transform;
use bevy_window::{CursorGrabMode, CursorOptions, PrimaryWindow, Window};

/// WASD + Q/E + mouse-look fly camera tag. Movement speed and look
/// sensitivity are tunable per entity.
#[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component)]
pub struct FlyCam {
    /// Base horizontal movement speed (world units / second).
    pub speed: f32,
    /// Multiplier applied while Shift is held.
    pub boost: f32,
    /// Mouse-look sensitivity (radians / pixel).
    pub sensitivity: f32,
    /// Whether mouse motion controls the camera. Disabled when the cursor is
    /// not locked, so UI clicks don't drag the view.
    pub mouse_look: bool,
}

impl Default for FlyCam {
    fn default() -> Self {
        Self {
            speed: 60.0,
            boost: 4.0,
            sensitivity: 0.003,
            mouse_look: true,
        }
    }
}

/// Plugin: registers the fly-camera and cursor-grab systems.
pub struct CameraPlugin;

impl Plugin for CameraPlugin {
    fn build(&self, app: &mut App) {
        app.register_type::<FlyCam>();
        app.add_systems(Update, (fly_camera, cursor_grab));
    }
}

/// Move every [`FlyCam`] entity according to keyboard / mouse input.
pub fn fly_camera(
    time: Res<Time>,
    keyboard: Res<ButtonInput<KeyCode>>,
    mouse: Res<AccumulatedMouseMotion>,
    cursor_q: Query<&CursorOptions, With<PrimaryWindow>>,
    mut q: Query<(&mut Transform, &FlyCam)>,
) {
    let dt = time.delta_secs();
    let cursor_locked = cursor_q
        .single()
        .map(|c| matches!(c.grab_mode, CursorGrabMode::Locked))
        .unwrap_or(false);

    for (mut transform, cam) in q.iter_mut() {
        let forward = Vec3::from(transform.forward());
        let right = Vec3::from(transform.right());

        let mut velocity = Vec3::ZERO;
        if keyboard.pressed(KeyCode::KeyW) {
            velocity += forward;
        }
        if keyboard.pressed(KeyCode::KeyS) {
            velocity -= forward;
        }
        if keyboard.pressed(KeyCode::KeyA) {
            velocity -= right;
        }
        if keyboard.pressed(KeyCode::KeyD) {
            velocity += right;
        }
        if keyboard.pressed(KeyCode::KeyE) {
            velocity.y += 1.0;
        }
        if keyboard.pressed(KeyCode::KeyQ) {
            velocity.y -= 1.0;
        }
        if velocity != Vec3::ZERO {
            velocity = velocity.normalize();
        }

        let boost = if keyboard.pressed(KeyCode::ShiftLeft)
            || keyboard.pressed(KeyCode::ShiftRight)
        {
            cam.boost
        } else {
            1.0
        };
        transform.translation += velocity * cam.speed * boost * dt;

        if cam.mouse_look && cursor_locked && mouse.delta != bevy_math::Vec2::ZERO {
            let yaw = -mouse.delta.x * cam.sensitivity;
            let pitch = -mouse.delta.y * cam.sensitivity;
            transform.rotate_y(yaw);
            transform.rotate_local_x(pitch);
        }
    }
}

/// Click to grab the cursor, Esc to release. Operates on the primary window.
pub fn cursor_grab(
    mut cursor_q: Query<&mut CursorOptions, With<PrimaryWindow>>,
    mouse: Res<ButtonInput<MouseButton>>,
    keys: Res<ButtonInput<KeyCode>>,
) {
    let Ok(mut cursor) = cursor_q.single_mut() else {
        return;
    };
    if mouse.just_pressed(MouseButton::Left) {
        cursor.grab_mode = CursorGrabMode::Locked;
        cursor.visible = false;
    }
    if keys.just_pressed(KeyCode::Escape) {
        cursor.grab_mode = CursorGrabMode::None;
        cursor.visible = true;
    }
}

// Keep bevy_window's Window type alive for the doc-link above.
#[allow(dead_code)]
type _KeepWindow = Window;
