//! Camera system.
//!
//! One persistent camera (spawned at [`Startup`]) is the sole source of truth
//! across every [`AppState`](crate::state::AppState). Scenes *reconfigure* it
//! by writing its [`OrbitCamera`] component and the [`CameraState`] resource —
//! they never spawn or despawn a camera. This keeps the single-camera invariant
//! that downstream systems (which use `Query<...>::single_mut()` on
//! [`MainCamera`]) rely on.
//!
//! # Model (per the *Camera System* design doc)
//! There are two **framing modes** — what the camera points at:
//! - [`CameraMode::Orbit`]: tactical inspection around a fixed target. Used by
//!   the Galaxy inspection scene, starting-base selection, and the docked
//!   in-system view.
//! - [`CameraMode::Follow`]: tracks a moving entity (the player ship) during
//!   flight.
//!
//! [`CinematicOverlay`] is **not** a framing mode — it is an optional overlay
//! (free rotation + hidden HUD + live gameplay) that can be toggled on top of
//! either framing at any time. This matches the doc's two operational states
//! (Docked / In-Transit) plus Cinematic-as-overlay, rather than three exclusive
//! modes.
//!
//! Smooth reframing is handled by [`OrbitFocusGoal`]: a scene arms it with the
//! desired target / distance / rotation and [`orbit_camera_control`] eases the
//! camera toward it, cancelling on user input.

use bevy::ecs::system::SystemParam;
use bevy::input::mouse::{MouseMotion, MouseWheel, MouseButton};
use bevy::prelude::*;
use bevy::window::PrimaryWindow;

use crate::gameplay::in_system::ActionUi;
use crate::ui::util::cursor_over_ui;

#[derive(Component)]
pub struct MainCamera;

/// Framing mode: how the camera decides what to point at.
///
/// See the module docs for the full model. [`CinematicOverlay`] is separate.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum CameraMode {
    /// Tactical inspection around a fixed target (Galaxy, starting-base, docked).
    #[default]
    Orbit,
    /// Track a moving entity (the player ship) during flight.
    Follow,
}

impl CameraMode {
    pub fn is_orbit(&self) -> bool {
        matches!(self, Self::Orbit)
    }
}

/// Canonical isometric "tactical" framing. The Camera System doc frames the
/// camera as a command-grade isometric viewport; every framing shares this
/// baseline angle, and Cinematic "detaches" from it by letting the user
/// rotate freely. Composed as `yaw * pitch` so it composes the same way the
/// incremental drag rotations do.
const TACTICAL_YAW: f32 = 0.15;
const TACTICAL_PITCH: f32 = 0.625;
pub fn tactical_rotation() -> Quat {
    Quat::from_rotation_y(TACTICAL_YAW) * Quat::from_rotation_x(TACTICAL_PITCH)
}

/// Default follow-camera framing: a high iso angle behind the ship.
const FOLLOW_DISTANCE: f32 = 45.0;
const FOLLOW_HEIGHT: f32 = 35.0;

/// Docked tactical framing: pull back far enough to see the station and the
/// docked ship together.
pub const DOCKED_FRAMING_DISTANCE: f32 = 18.0;

/// Focus-tween speed (exponential-damp). ~6.0 settles over roughly a second,
/// matching the idiom used elsewhere in the codebase.
const FOCUS_TWEEN_SPEED: f32 = 6.0;
/// Snap-to-exact and release the focus goal once a field is within this
/// tolerance. Without it the asymptotic damp would never release.
const FOCUS_EPSILON: f32 = 0.5;

const ORBIT_ROTATE_SENSITIVITY: f32 = 0.005;
const ORBIT_ZOOM_SENSITIVITY: f32 = 0.1;
/// Wide bounds so the same clamp serves both the galaxy scale (default distance
/// ~420) and the in-system scale (docked ~18, follow ~45).
const ORBIT_MIN_DISTANCE: f32 = 2.0;
const ORBIT_MAX_DISTANCE: f32 = 1500.0;

/// Global camera state. Drives which framing is active and — for Follow mode —
/// which entity to track and at what offset.
#[derive(Resource, Debug, Clone, Copy)]
pub struct CameraState {
    pub mode: CameraMode,
    /// Entity the camera focuses on. In Orbit mode this is the inspected
    /// target (station/planet); in Follow mode it is the tracked ship.
    /// [`track_camera_target`] keeps [`OrbitCamera::target`] locked to it.
    pub target_entity: Option<Entity>,
    pub follow_distance: f32,
    pub follow_height: f32,
}

impl Default for CameraState {
    fn default() -> Self {
        // Non-zero defaults so entering Follow without explicitly configuring
        // the offsets never collapses the camera onto the ship's origin.
        Self {
            mode: CameraMode::Orbit,
            target_entity: None,
            follow_distance: FOLLOW_DISTANCE,
            follow_height: FOLLOW_HEIGHT,
        }
    }
}

/// Cinematic overlay toggle. When active the HUD is hidden and the camera can
/// orbit freely around the current focal target regardless of framing mode.
/// Underlying gameplay keeps running — this is a *viewing* mode, not a pause.
/// Toggled by [`toggle_cinematic`].
#[derive(Resource, Debug, Clone, Copy, Default)]
pub struct CinematicOverlay {
    pub active: bool,
}

/// A goal [`orbit_camera_control`] eases the camera toward. Each field is
/// optional: only the `Some` fields are tweened, so a scene can refocus just
/// the distance (e.g.) without disturbing a user-chosen angle.
///
/// The goal auto-clears field-by-field as each arrives, and is cancelled
/// entirely the moment the user takes manual control (drag / scroll).
///
/// Contract: in [`AppState::InGame`](crate::state::AppState::InGame) the
/// `target` field is left `None` because [`track_camera_target`] owns the
/// target there; inspection scenes (Galaxy / starting-base) set `target`.
#[derive(Resource, Debug, Clone, Copy, Default)]
pub struct OrbitFocusGoal {
    pub target: Option<Vec3>,
    pub distance: Option<f32>,
    pub rotation: Option<Quat>,
    pub active: bool,
}

impl OrbitFocusGoal {
    /// Arm a new goal, fully replacing any previous one (so no stale field
    /// carries over from an earlier scene).
    pub fn arm(&mut self, target: Option<Vec3>, distance: Option<f32>, rotation: Option<Quat>) {
        *self = Self {
            target,
            distance,
            rotation,
            active: true,
        };
    }
}

/// Orbit-style camera: rotates around `target` at `distance`, controlled by
/// mouse. Orientation is stored as a quaternion (`rotation`) to allow true
/// 360° motion with no gimbal lock.
///
/// The base orientation is the camera at `target + rotation * (0,0,distance)`
/// looking toward `target`. Mouse drag applies incremental rotations:
///   - horizontal delta → yaw around world `+Y`
///   - vertical delta   → pitch around the camera's local `+X`
///
/// This yaw-around-world / pitch-around-local split is the standard free-look
/// construction; it never produces a degenerate "up" vector at the poles.
#[derive(Component, Debug, Clone, Copy)]
pub struct OrbitCamera {
    pub target: Vec3,
    pub distance: f32,
    pub rotation: Quat,
}

impl OrbitCamera {
    /// Reset to default orientation.
    pub fn reset(&mut self) {
        *self = Self::default();
    }
}

impl Default for OrbitCamera {
    fn default() -> Self {
        Self {
            target: Vec3::ZERO,
            distance: 420.0,
            rotation: tactical_rotation(),
        }
    }
}

/// The single persistent camera spawn. Runs once at [`Startup`]; this same
/// entity is reconfigured by every scene thereafter.
pub fn spawn_camera(mut commands: Commands) {
    let orbit = OrbitCamera::default();
    let offset = orbit.rotation * Vec3::new(0.0, 0.0, orbit.distance);
    commands.spawn((
        Camera3d::default(),
        Transform::from_translation(orbit.target + offset)
            .with_rotation(orbit.rotation)
            .looking_at(orbit.target, Vec3::Y),
        MainCamera,
        orbit,
        Camera {
            clear_color: ClearColorConfig::Custom(Color::srgb(0.02, 0.02, 0.05)),
            ..default()
        },
    ));
}

/// Bundles the read-mostly camera dynamics resources so [`orbit_camera_control`]
/// stays under clippy's argument-count threshold while still touching the focus
/// goal mutably.
#[derive(SystemParam)]
pub struct CameraDynamics<'w> {
    state: Res<'w, CameraState>,
    cinematic: Res<'w, CinematicOverlay>,
    goal: Option<ResMut<'w, OrbitFocusGoal>>,
    time: Res<'w, Time>,
}

/// The single camera positioning + control system. Runs in every 3D scene
/// (Galaxy, StartingBaseSelection, InGame). It:
/// 1. applies mouse input (drag → rotation, scroll → zoom) when input is
///    allowed — always in Orbit framing, or in any framing while the
///    [`CinematicOverlay`] is active;
/// 2. eases toward an armed [`OrbitFocusGoal`] (cancelled by manual input);
/// 3. clamps the zoom distance; and
/// 4. writes the camera [`Transform`] from `target + rotation * (0,0,distance)`.
///
/// Manual input is suppressed when the cursor is over any UI node, so clicking
/// buttons/panels never tumbles the camera.
pub fn orbit_camera_control(
    mut dynamics: CameraDynamics,
    mouse_input: Res<ButtonInput<MouseButton>>,
    primary_window: Query<&Window, With<PrimaryWindow>>,
    mut mouse_motion: EventReader<MouseMotion>,
    mut scroll_events: EventReader<MouseWheel>,
    mut query: Query<(&mut Transform, &mut OrbitCamera), With<MainCamera>>,
    ui_nodes: Query<(&ComputedNode, &GlobalTransform)>,
) {
    let Ok((mut transform, mut orbit)) = query.single_mut() else {
        // Drain pending input to avoid stale buildup when there's no camera.
        mouse_motion.clear();
        scroll_events.clear();
        return;
    };

    // Free-look input is allowed in Orbit framing, or in any framing while the
    // cinematic overlay is on.
    let allow_input = dynamics.state.mode.is_orbit() || dynamics.cinematic.active;

    let cursor_over_ui = primary_window
        .single()
        .ok()
        .map(|w| cursor_over_ui(w, &ui_nodes))
        .unwrap_or(false);

    // Track whether the user actively manipulated the camera so an in-flight
    // focus tween can be cancelled. Input is only read when allowed.
    let mut manual_input = false;
    if allow_input && mouse_input.pressed(MouseButton::Left) && !cursor_over_ui {
        for event in mouse_motion.read() {
            manual_input = true;
            // iPhone-style: content follows the finger horizontally.
            let yaw = Quat::from_rotation_y(-event.delta.x * ORBIT_ROTATE_SENSITIVITY);
            // Vertical: drag down raises the camera so the scene shifts down.
            let pitch = Quat::from_rotation_x(event.delta.y * ORBIT_ROTATE_SENSITIVITY);
            // Pre-multiply yaw (world axis), post-multiply pitch (local axis).
            orbit.rotation = yaw * orbit.rotation * pitch;
            // Re-normalize to counteract floating-point drift.
            orbit.rotation = orbit.rotation.normalize();
        }
    } else {
        mouse_motion.clear();
    }

    if allow_input {
        for event in scroll_events.read() {
            if cursor_over_ui {
                continue;
            }
            manual_input = true;
            // Scroll up (positive y) → decrease distance (zoom in).
            orbit.distance *= 1.0 - event.y * ORBIT_ZOOM_SENSITIVITY;
        }
    } else {
        scroll_events.clear();
    }

    // Manual input immediately cancels any pending focus tween so control
    // returns to the user.
    if manual_input {
        if let Some(goal) = dynamics.goal.as_deref_mut() {
            goal.active = false;
        }
    }

    // Ease toward the focus goal (only the armed fields).
    if let Some(goal) = dynamics.goal.as_deref_mut() {
        if goal.active {
            let dt = dynamics.time.delta_secs().min(0.1);
            let alpha = (dt * FOCUS_TWEEN_SPEED).clamp(0.0, 1.0);

            if let Some(target) = goal.target {
                let delta = target - orbit.target;
                orbit.target += delta * alpha;
                if delta.length() < FOCUS_EPSILON {
                    orbit.target = target;
                    goal.target = None;
                }
            }
            if let Some(distance) = goal.distance {
                let delta = distance - orbit.distance;
                orbit.distance += delta * alpha;
                if delta.abs() < FOCUS_EPSILON {
                    orbit.distance = distance;
                    goal.distance = None;
                }
            }
            if let Some(rotation) = goal.rotation {
                orbit.rotation = orbit.rotation.slerp(rotation, alpha);
                // Quaternions q and -q are the same rotation; compare |dot|.
                if orbit.rotation.dot(rotation).abs() > 1.0 - 1e-4 {
                    orbit.rotation = rotation;
                    goal.rotation = None;
                }
            }

            if goal.target.is_none() && goal.distance.is_none() && goal.rotation.is_none() {
                goal.active = false;
            }
        }
    }

    orbit.distance = orbit.distance.clamp(ORBIT_MIN_DISTANCE, ORBIT_MAX_DISTANCE);

    // `orbit.rotation` already encodes the correct look direction
    // (`rotation * -Z` points toward the target); it is the sole source of
    // truth, so do not recompute via `looking_at`.
    let offset = orbit.rotation * Vec3::new(0.0, 0.0, orbit.distance);
    let position = orbit.target + offset;
    *transform = Transform::from_translation(position).with_rotation(orbit.rotation);
}

/// Keep [`OrbitCamera::target`] locked to [`CameraState::target_entity`].
///
/// This unifies follow (ship) and docked (station) tracking: whichever entity
/// the camera is focused on, its world position becomes the orbit target each
/// frame, so switching framings never leaves the target stale (gap B6). It runs
/// in [`PostUpdate`](bevy::app::PostUpdate) after transform propagation so the
/// target's [`GlobalTransform`] is current.
///
/// In InGame the focus goal never tweens the target (scenes arm only
/// distance/rotation there), so there is no conflict with [`OrbitFocusGoal`].
pub fn track_camera_target(
    camera_state: Res<CameraState>,
    mut camera_query: Query<&mut OrbitCamera, With<MainCamera>>,
    target_query: Query<&GlobalTransform>,
) {
    let Some(target_entity) = camera_state.target_entity else {
        return;
    };
    let Ok(mut orbit) = camera_query.single_mut() else {
        return;
    };
    let Ok(target_transform) = target_query.get(target_entity) else {
        return;
    };
    orbit.target = target_transform.translation();
}

/// Resource: set by UI buttons (e.g. the Galaxy "Center View" button) to
/// request the orbit camera snap back to its default orientation next frame.
/// Consumed by [`apply_orbit_reset`].
#[derive(Resource, Default, Debug)]
pub struct ResetOrbitCamera;

/// If [`ResetOrbitCamera`] is present, snap the orbit camera back to default
/// and consume the resource.
pub fn apply_orbit_reset(
    mut commands: Commands,
    flag: Option<Res<ResetOrbitCamera>>,
    mut query: Query<&mut OrbitCamera, With<MainCamera>>,
) {
    let Some(_flag) = flag else {
        return;
    };
    let Ok(mut orbit) = query.single_mut() else {
        commands.remove_resource::<ResetOrbitCamera>();
        return;
    };
    orbit.reset();
    commands.remove_resource::<ResetOrbitCamera>();
}

/// Toggle the [`CinematicOverlay`] on the cinematic key. Available in any
/// in-game framing (docked or in transit), per the design doc.
pub fn toggle_cinematic(
    keys: Res<ButtonInput<KeyCode>>,
    mut cinematic: ResMut<CinematicOverlay>,
) {
    if keys.just_pressed(KeyCode::KeyC) {
        cinematic.active = !cinematic.active;
        bevy::log::info!(
            "Cinematic overlay {}",
            if cinematic.active { "enabled" } else { "disabled" }
        );
    }
}

/// Hide the in-game HUD while the [`CinematicOverlay`] is active, restore it
/// otherwise. The doc specifies Cinematic leaves "an unobstructed view."
pub fn update_cinematic_hud(
    cinematic: Res<CinematicOverlay>,
    mut hud: Query<&mut Visibility, With<ActionUi>>,
) {
    let Ok(mut visibility) = hud.single_mut() else {
        return;
    };
    let target = if cinematic.active {
        Visibility::Hidden
    } else {
        Visibility::Visible
    };
    if *visibility != target {
        *visibility = target;
    }
}

/// Reset transient camera state when leaving a camera-driven scene, so a
/// half-finished focus tween or a left-on cinematic overlay can't bleed into
/// the next scene. Registered on the `OnExit` of the scenes that arm them.
pub fn reset_transient_camera_state(
    mut goal: ResMut<OrbitFocusGoal>,
    mut cinematic: ResMut<CinematicOverlay>,
) {
    goal.active = false;
    cinematic.active = false;
}

/// Reconfigure the persistent camera for a fresh framing. Used by scenes on
/// enter (e.g. the docked view) to hard-set the orbit target / distance /
/// rotation in one shot. A hard set (rather than a tween) is correct for scene
/// *transitions* where the scale changes discontinuously (galaxy → in-system);
/// use [`OrbitFocusGoal`] for smooth reframing *within* a scene.
pub fn retarget_main_camera(
    camera_query: &mut Query<&mut OrbitCamera, With<MainCamera>>,
    target: Vec3,
    distance: f32,
    rotation: Quat,
) {
    if let Ok(mut orbit) = camera_query.single_mut() {
        orbit.target = target;
        orbit.distance = distance;
        orbit.rotation = rotation;
    }
}
