//! Kinematic collision resolution and proximity queries.
//!
//! Per ARCH-7 there is no physics engine: movement is point-and-shoot and
//! collision is distance-based. This module wires the pure geometry in
//! [`super::geometry`] into Bevy systems that run each frame during gameplay.
//!
//! ## Collision pipeline
//!
//! 1. [`detect_collisions`] snapshots every physical body (anything with a
//!    [`BoundingVolume`]) and runs an O(n²) broad+narrow phase, computing
//!    mass-weighted position corrections and velocity cancels into
//!    [`FrameCollisions`], and emitting [`CollisionEvent`]s.
//! 2. [`resolve_collisions`] drains [`FrameCollisions`] and applies the
//!    corrections to `Transform` and `Velocity`.
//!
//! The split exists so detection (read-only) and resolution (write) never alias
//! the same component in a single system. A body is "movable" iff it has a
//! [`Velocity`]; everything else (planets, stations, the star, asteroid belts)
//! is immovable scenery the ship bounces off. Ship-to-ship separation is
//! weighted by inverse [`BodyMass`].
//!
//! ## Spatial queries
//!
//! [`update_proximity_sensors`] advances every [`ProximitySensor`], detecting
//! bodies entering/leaving its radius and emitting [`ProximityEvent`]s — the
//! building block for docking-range, mining-range, and mass-lock checks.
//!
//! ## Hierarchy assumption
//!
//! In-system entities are children of the scene root at the origin, so local
//! `Transform` equals world space. World-space corrections are therefore
//! applied directly to `Transform.translation`. This matches the existing
//! convention in `movement::orbit` and `galaxy::orbits`.

use std::collections::HashMap;

use bevy::prelude::*;

use crate::gameplay::galaxy::BoundingVolume;
use crate::gameplay::movement::components::Velocity;

use super::components::{BodyMass, CollisionEvent, ProximityEvent, ProximitySensor};
use super::geometry::{sphere_overlap, split_by_inverse_mass};

/// Minimum mass floor so inverse-mass never divides by zero.
const MASS_FLOOR: f32 = 1e-6;

/// Per-frame collision corrections: produced by [`detect_collisions`] and
/// consumed by [`resolve_collisions`].
#[derive(Resource, Default, Debug)]
pub struct FrameCollisions {
    /// `(entity, world-space delta)` to add to each body's translation.
    pub pushes: HashMap<Entity, Vec3>,
    /// `(entity, world-space delta)` to subtract from each body's velocity.
    pub cancels: HashMap<Entity, Vec3>,
}

/// Read-only snapshot of a body used by the pairwise resolver.
#[derive(Debug, Clone, Copy)]
struct BodySnapshot {
    entity: Entity,
    pos: Vec3,
    radius: f32,
    mass: f32,
    movable: bool,
    velocity: Vec3,
}

/// A planned contact, kept around to emit [`CollisionEvent`]s.
#[derive(Debug, Clone, Copy)]
struct PlannedContact {
    a: Entity,
    b: Entity,
    normal: Vec3,
    penetration: f32,
}

/// Pure output of [`plan_collisions`].
#[derive(Default)]
struct CollisionPlan {
    pushes: HashMap<Entity, Vec3>,
    cancels: HashMap<Entity, Vec3>,
    contacts: Vec<PlannedContact>,
}

impl CollisionPlan {
    fn add_push(&mut self, entity: Entity, delta: Vec3) {
        self.pushes
            .entry(entity)
            .and_modify(|p| *p += delta)
            .or_insert(delta);
    }

    fn add_cancel(&mut self, entity: Entity, delta: Vec3) {
        self.cancels
            .entry(entity)
            .and_modify(|c| *c += delta)
            .or_insert(delta);
    }
}

/// Snapshot physical bodies and compute the collision plan for this frame.
///
/// Pure (no ECS access) so it can be unit-tested directly.
fn plan_collisions(bodies: &[BodySnapshot]) -> CollisionPlan {
    let mut plan = CollisionPlan::default();
    for (i, a) in bodies.iter().enumerate() {
        for b in &bodies[i + 1..] {
            if !a.movable && !b.movable {
                continue;
            }
            let Some((penetration, normal)) = sphere_overlap(a.pos, a.radius, b.pos, b.radius)
            else {
                continue;
            };

            let inv_a = if a.movable { 1.0 / a.mass } else { 0.0 };
            let inv_b = if b.movable { 1.0 / b.mass } else { 0.0 };
            let (push_a, push_b) = split_by_inverse_mass(normal, penetration, inv_a, inv_b);
            // Only movable bodies accrue positional correction; immovable
            // scenery is never displaced.
            if a.movable {
                plan.add_push(a.entity, push_a);
            }
            if b.movable {
                plan.add_push(b.entity, push_b);
            }

            // Cancel the velocity component driving each movable body into the
            // other, so ships slide along obstacles instead of vibrating.
            if a.movable {
                let inbound = a.velocity.dot(normal);
                if inbound > 0.0 {
                    plan.add_cancel(a.entity, normal * inbound);
                }
            }
            if b.movable {
                let toward_a = -normal;
                let inbound = b.velocity.dot(toward_a);
                if inbound > 0.0 {
                    plan.add_cancel(b.entity, toward_a * inbound);
                }
            }

            plan.contacts.push(PlannedContact {
                a: a.entity,
                b: b.entity,
                normal,
                penetration,
            });
        }
    }
    plan
}

/// Snapshot physical bodies, compute the collision plan, stash it in
/// [`FrameCollisions`], and emit [`CollisionEvent`]s for every contact.
///
/// Three narrow queries keep the system's component access simple and
/// conflict-free (position/radius, mass, and velocity are disjoint
/// components), looked up per-entity while building snapshots.
pub fn detect_collisions(
    bodies_query: Query<(Entity, &GlobalTransform, &BoundingVolume)>,
    masses: Query<&BodyMass>,
    velocities: Query<&Velocity>,
    mut frame: ResMut<FrameCollisions>,
    mut events: EventWriter<CollisionEvent>,
) {
    let bodies: Vec<BodySnapshot> = bodies_query
        .iter()
        .map(|(entity, gtf, volume)| {
            let movable = velocities.get(entity).is_ok();
            let mass = masses
                .get(entity)
                .map(|m| m.0)
                .unwrap_or(1.0)
                .max(MASS_FLOOR);
            let velocity = velocities.get(entity).map(|v| v.0).unwrap_or(Vec3::ZERO);
            BodySnapshot {
                entity,
                pos: gtf.translation(),
                radius: volume.radius,
                mass,
                movable,
                velocity,
            }
        })
        .collect();

    let plan = plan_collisions(&bodies);
    for contact in &plan.contacts {
        events.write(CollisionEvent {
            a: contact.a,
            b: contact.b,
            normal: contact.normal,
            penetration: contact.penetration,
        });
    }

    frame.pushes = plan.pushes;
    frame.cancels = plan.cancels;
}

/// Apply the position and velocity corrections computed by
/// [`detect_collisions`].
pub fn resolve_collisions(
    mut frame: ResMut<FrameCollisions>,
    mut transforms: Query<&mut Transform>,
    mut velocities: Query<&mut Velocity>,
) {
    let pushes = std::mem::take(&mut frame.pushes);
    for (entity, delta) in pushes {
        if let Ok(mut transform) = transforms.get_mut(entity) {
            transform.translation += delta;
        }
    }

    let cancels = std::mem::take(&mut frame.cancels);
    for (entity, delta) in cancels {
        if let Ok(mut velocity) = velocities.get_mut(entity) {
            velocity.0 -= delta;
        }
    }
}

/// Advance each [`ProximitySensor`]: detect physical bodies entering or leaving
/// its radius and emit [`ProximityEvent`]s. A body is "inside" when the gap
/// between their centers is within `sensor.radius + body.radius`.
pub fn update_proximity_sensors(
    mut sensors: Query<(Entity, &GlobalTransform, &mut ProximitySensor)>,
    bodies: Query<(Entity, &GlobalTransform, &BoundingVolume)>,
    mut events: EventWriter<ProximityEvent>,
) {
    for (sensor_entity, sensor_transform, mut sensor) in sensors.iter_mut() {
        let center = sensor_transform.translation();
        let radius = sensor.radius;

        let current: Vec<Entity> = bodies
            .iter()
            .filter_map(|(entity, gtf, volume)| {
                if entity == sensor_entity {
                    return None;
                }
                let reach = radius + volume.radius;
                if center.distance(gtf.translation()) <= reach {
                    Some(entity)
                } else {
                    None
                }
            })
            .collect();

        let previous = std::mem::replace(&mut sensor.occupants, current);

        // Enter: present now, absent last frame.
        for &entity in &sensor.occupants {
            if !previous.contains(&entity) {
                events.write(ProximityEvent::Enter {
                    sensor: sensor_entity,
                    other: entity,
                });
            }
        }
        // Exit: present last frame, absent now.
        for &entity in &previous {
            if !sensor.occupants.contains(&entity) {
                events.write(ProximityEvent::Exit {
                    sensor: sensor_entity,
                    other: entity,
                });
            }
        }
    }
}

/// Debug observer for the physics pipeline.
///
/// Reads [`CollisionEvent`] and [`ProximityEvent`] each frame and logs them,
/// so the collision and spatial-query systems are observable before combat /
/// docking systems are wired up to consume the same events. Collisions are
/// logged at `debug` (they fire every frame an overlap persists); proximity
/// transitions are logged at `info` (they fire only on Enter/Exit).
pub fn log_physics_events(
    mut collisions: EventReader<CollisionEvent>,
    mut proximity: EventReader<ProximityEvent>,
) {
    for event in collisions.read() {
        bevy::log::debug!(
            "collision: {:?} <-> {:?} penetration={:.3} normal={:?}",
            event.a, event.b, event.penetration, event.normal
        );
    }
    for event in proximity.read() {
        match *event {
            ProximityEvent::Enter { sensor, other } => {
                bevy::log::info!("proximity: {other:?} entered sensor {sensor:?}");
            }
            ProximityEvent::Exit { sensor, other } => {
                bevy::log::info!("proximity: {other:?} left sensor {sensor:?}");
            }
        }
    }
}

// ── Tests ───────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    fn snap(index: u32, x: f32, radius: f32, movable: bool, vel_x: f32) -> BodySnapshot {
        BodySnapshot {
            entity: Entity::from_raw(index),
            pos: Vec3::new(x, 0.0, 0.0),
            radius,
            mass: 1.0,
            movable,
            velocity: Vec3::new(vel_x, 0.0, 0.0),
        }
    }

    #[test]
    fn equal_mass_splits_correction_and_cancels_inbound_velocity() {
        // a at x=0 (r=1) moving +X; b at x=1 (r=1) moving -X. Overlap 1.0.
        let bodies = [snap(0, 0.0, 1.0, true, 1.0), snap(1, 1.0, 1.0, true, -1.0)];
        let plan = plan_collisions(&bodies);

        assert!(
            (plan.pushes[&Entity::from_raw(0)] - Vec3::new(-0.5, 0.0, 0.0)).length() < 1e-5,
            "a should be pushed back half the overlap"
        );
        assert!(
            (plan.pushes[&Entity::from_raw(1)] - Vec3::new(0.5, 0.0, 0.0)).length() < 1e-5,
            "b should be pushed forward half the overlap"
        );
        assert!(
            (plan.cancels[&Entity::from_raw(0)] - Vec3::new(1.0, 0.0, 0.0)).length() < 1e-5,
            "a's inbound velocity should be canceled"
        );
        assert!(
            (plan.cancels[&Entity::from_raw(1)] - Vec3::new(-1.0, 0.0, 0.0)).length() < 1e-5,
            "b's inbound velocity should be canceled"
        );
        assert_eq!(plan.contacts.len(), 1);
    }

    #[test]
    fn static_body_takes_full_push_and_none_itself() {
        // Movable a vs immovable b: a absorbs the full correction.
        let bodies = [snap(0, 0.0, 1.0, true, 1.0), snap(1, 1.0, 1.0, false, 0.0)];
        let plan = plan_collisions(&bodies);

        assert!(
            (plan.pushes[&Entity::from_raw(0)] - Vec3::new(-1.0, 0.0, 0.0)).length() < 1e-5
        );
        assert!(plan.pushes.get(&Entity::from_raw(1)).is_none());
    }

    #[test]
    fn no_overlap_produces_empty_plan() {
        let bodies = [snap(0, 0.0, 1.0, true, 1.0), snap(1, 5.0, 1.0, true, 0.0)];
        let plan = plan_collisions(&bodies);
        assert!(plan.pushes.is_empty());
        assert!(plan.cancels.is_empty());
        assert!(plan.contacts.is_empty());
    }

    #[test]
    fn receding_body_keeps_its_velocity() {
        // a moving away from b (-X) should not have velocity canceled.
        let bodies = [snap(0, 0.0, 1.0, true, -1.0), snap(1, 1.0, 1.0, false, 0.0)];
        let plan = plan_collisions(&bodies);
        assert!(plan.cancels.is_empty());
    }

    #[test]
    fn pair_of_immovable_bodies_is_skipped() {
        let bodies = [snap(0, 0.0, 1.0, false, 0.0), snap(1, 0.5, 1.0, false, 0.0)];
        let plan = plan_collisions(&bodies);
        assert!(plan.pushes.is_empty());
        assert!(plan.contacts.is_empty());
    }

    #[test]
    fn heavier_body_moves_less() {
        // a heavy (mass 9), b light (mass 1), equal inverse split → a moves 1/10.
        let mut a = snap(0, 0.0, 1.0, true, 0.0);
        a.mass = 9.0;
        let mut b = snap(1, 1.0, 1.0, true, 0.0);
        b.mass = 1.0;
        let bodies = [a, b];
        let plan = plan_collisions(&bodies);

        // overlap 1.0; a share = inv_a/(inv_a+inv_b) = (1/9)/(1/9+1) = 0.1
        assert!(
            (plan.pushes[&Entity::from_raw(0)] - Vec3::new(-0.1, 0.0, 0.0)).length() < 1e-5
        );
        assert!(
            (plan.pushes[&Entity::from_raw(1)] - Vec3::new(0.9, 0.0, 0.0)).length() < 1e-5
        );
    }
}
