//! Real 2D rigid-body dynamics via rapier (item 17 of the v0.4 sweep),
//! behind the `physics` feature.
//!
//! The tier ABOVE collide.rs: collide.rs stays the zero-dep teaching layer
//! (overlap, MTV, you write the response); this module is what you register
//! when a game needs forces, stacking, restitution, and solver-grade
//! contacts - the things the guides said were "rapier's job". The engine
//! shape is unchanged: bodies are COMPONENTS (`DynamicBody`, `StaticBody`),
//! the simulation is a struct you step, and a thin `PhysicsModule` plugs it
//! into the frame loop like audio did.
//!
//! Conventions: Mawida 2D is y-DOWN, so gravity defaults to +981 px/s^2
//! (down the screen). A `Transform`'s translation is a sprite's TOP-LEFT;
//! rapier thinks in centers; the conversion happens here, at the border,
//! exactly once, in each direction.

use std::collections::HashMap;

use rapier2d::prelude::*;

use crate::math::Vec2 as MVec2;
use crate::transform::Transform;
use crate::world::{Entity, World};

/// A dynamic (simulated) box body: gravity, collisions, restitution.
/// Pair it with a `Transform`; the physics writes the transform back.
#[derive(Debug, Clone, Copy)]
pub struct DynamicBody {
    /// Half extents of the collision box, pixels.
    pub half: MVec2,
    /// Bounciness, 0 (dead stop) to 1 (perfect bounce).
    pub restitution: f32,
}

/// A static (immovable) box body: floors, walls, platforms.
#[derive(Debug, Clone, Copy)]
pub struct StaticBody {
    pub half: MVec2,
}

/// The simulation: rapier's sets and pipelines plus the Entity -> body map.
/// Pure logic (no GPU, no window): headless-testable like the World itself.
pub struct Physics {
    /// Pixels per second squared. +y is DOWN in Mawida's 2D.
    pub gravity: MVec2,
    bodies: RigidBodySet,
    colliders: ColliderSet,
    pipeline: PhysicsPipeline,
    islands: IslandManager,
    broad_phase: DefaultBroadPhase,
    narrow_phase: NarrowPhase,
    impulse_joints: ImpulseJointSet,
    multibody_joints: MultibodyJointSet,
    ccd: CCDSolver,
    params: IntegrationParameters,
    map: HashMap<u64, RigidBodyHandle>, // Entity::to_bits -> body
}

impl Default for Physics {
    fn default() -> Self {
        Self::new()
    }
}

impl Physics {
    pub fn new() -> Self {
        Self {
            gravity: MVec2::new(0.0, 981.0), // y-down: gravity points DOWN the screen
            bodies: RigidBodySet::new(),
            colliders: ColliderSet::new(),
            pipeline: PhysicsPipeline::new(),
            islands: IslandManager::new(),
            broad_phase: DefaultBroadPhase::new(),
            narrow_phase: NarrowPhase::new(),
            impulse_joints: ImpulseJointSet::new(),
            multibody_joints: MultibodyJointSet::new(),
            ccd: CCDSolver::new(),
            params: IntegrationParameters::default(),
            map: HashMap::new(),
        }
    }

    /// Advance the simulation and sync Transforms, one call:
    /// 1. entities carrying a body component but no rapier body yet get one
    ///    (top-left translation -> center, at the border);
    /// 2. rapier steps by `dt` (drive this from a FIXED system or the
    ///    module for determinism);
    /// 3. every dynamic body writes its center back as a top-left.
    pub fn step(&mut self, world: &mut World, dt: f32) {
        // -- 1. adopt new bodies ------------------------------------------
        let mut adopt: Vec<(u64, MVec2, MVec2, f32, bool)> = Vec::new();
        for (e, body) in world.iter::<DynamicBody>() {
            if !self.map.contains_key(&e.to_bits()) {
                if let Some(t) = world.get::<Transform>(e) {
                    let center = t.translation.truncate() + body.half;
                    adopt.push((e.to_bits(), center, body.half, body.restitution, true));
                }
            }
        }
        for (e, body) in world.iter::<StaticBody>() {
            if !self.map.contains_key(&e.to_bits()) {
                if let Some(t) = world.get::<Transform>(e) {
                    let center = t.translation.truncate() + body.half;
                    adopt.push((e.to_bits(), center, body.half, 0.0, false));
                }
            }
        }
        for (bits, center, half, restitution, dynamic) in adopt {
            let builder = if dynamic {
                RigidBodyBuilder::dynamic()
            } else {
                RigidBodyBuilder::fixed()
            };
            let handle = self
                .bodies
                .insert(builder.translation(rapier2d::math::Vector::new(center.x, center.y)).build());
            self.colliders.insert_with_parent(
                ColliderBuilder::cuboid(half.x, half.y)
                    .restitution(restitution)
                    .build(),
                handle,
                &mut self.bodies,
            );
            self.map.insert(bits, handle);
        }

        // -- 2. step ------------------------------------------------------
        self.params.dt = dt;
        self.pipeline.step(
            rapier2d::math::Vector::new(self.gravity.x, self.gravity.y),
            &self.params,
            &mut self.islands,
            &mut self.broad_phase,
            &mut self.narrow_phase,
            &mut self.bodies,
            &mut self.colliders,
            &mut self.impulse_joints,
            &mut self.multibody_joints,
            &mut self.ccd,
            &(),
            &(),
        );

        // -- 3. write back ------------------------------------------------
        // (also drop bodies whose entities despawned: the map is the truth)
        self.map.retain(|bits, handle| {
            let e = Entity::from_bits(*bits);
            if !world.is_alive(e) {
                self.bodies.remove(
                    *handle,
                    &mut self.islands,
                    &mut self.colliders,
                    &mut self.impulse_joints,
                    &mut self.multibody_joints,
                    true,
                );
                return false;
            }
            true
        });
        for (bits, handle) in &self.map {
            let e = Entity::from_bits(*bits);
            let Some(body) = self.bodies.get(*handle) else { continue };
            if !body.is_dynamic() {
                continue;
            }
            let half = match world.get::<DynamicBody>(e) {
                Some(b) => b.half,
                None => continue,
            };
            let p = body.translation();
            let rot = body.rotation().angle();
            if let Some(t) = world.get_mut::<Transform>(e) {
                t.translation.x = p.x - half.x; // center -> top-left, once
                t.translation.y = p.y - half.y;
                t.rotation = crate::math::Quat::from_rotation_z(rot);
            }
        }
    }
}

/// The frame-loop plug: physics as a module, like audio. Registers with
/// `.add_module(PhysicsModule::default())`; entities with body components
/// simulate, everything else ignores it.
#[cfg(feature = "runtime")]
#[derive(Default)]
pub struct PhysicsModule {
    pub physics: Physics,
}

#[cfg(feature = "runtime")]
impl crate::module::Module for PhysicsModule {
    fn update(&mut self, world: &mut World, dt: f32) {
        self.physics.step(world, dt);
    }
}

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

    #[test]
    fn a_box_falls_onto_the_floor_and_rests() {
        let mut world = World::new();

        // a floor at y = 500 (top-left convention, 400 wide, 20 tall)
        let floor = world.spawn();
        world.insert(floor, Transform::from_xy(-200.0, 500.0));
        world.insert(floor, StaticBody { half: MVec2::new(200.0, 10.0) });

        // a 20x20 box dropped from y = 100
        let crate_ = world.spawn();
        world.insert(crate_, Transform::from_xy(-10.0, 100.0));
        world.insert(crate_, DynamicBody { half: MVec2::splat(10.0), restitution: 0.0 });

        let mut physics = Physics::new();
        for _ in 0..180 {
            physics.step(&mut world, 1.0 / 60.0); // three simulated seconds
        }

        // It fell (y grew: y-down) and now RESTS on the floor: its top-left
        // y is the floor's top minus its height: 500 - 20 = 480.
        let t = world.get::<Transform>(crate_).unwrap();
        assert!(
            (t.translation.y - 480.0).abs() < 1.0,
            "crate should rest at y=480, got {}",
            t.translation.y
        );
    }

    #[test]
    fn despawned_bodies_leave_the_simulation() {
        let mut world = World::new();
        let e = world.spawn();
        world.insert(e, Transform::from_xy(0.0, 0.0));
        world.insert(e, DynamicBody { half: MVec2::splat(5.0), restitution: 0.0 });

        let mut physics = Physics::new();
        physics.step(&mut world, 1.0 / 60.0);
        assert_eq!(physics.map.len(), 1);

        world.despawn(e);
        physics.step(&mut world, 1.0 / 60.0);
        assert_eq!(physics.map.len(), 0); // the map is the truth
    }
}
