//! The [`PhysicsModule`] — the Stage-9 entry point that wires physics into an
//! [`App`].
//!
//! Following the engine's module convention, everything physics contributes is
//! registered here so it can be enabled, disabled, or removed as a unit (and so
//! an exported game that never touches physics never compiles it in). This piece
//! registers the component *types* for reflection (making [`RigidBody`] and
//! [`Collider`] dual-editable and captured by the play-mode snapshot) and
//! installs the global [`PhysicsSettings`]. The simulation system itself is
//! added in a later piece.

use oxide_engine::app::{App, Module, Schedule};
use oxide_engine::math::Vec3;

use crate::world::step_physics;
use crate::{CharacterController, Collider, PhysicsWorld, RigidBody};

/// Earth-like gravity (m/s²) along `-Y` — the default for a new physics world.
pub const DEFAULT_GRAVITY: Vec3 = Vec3::new(0.0, -9.81, 0.0);

/// Project-wide physics tuning, stored as an [`App`] resource.
///
/// Kept tiny and serializable so it can grow into a settings page; for now it
/// carries the global gravity vector the simulation integrates dynamic bodies
/// against (each body can still scale it via
/// [`RigidBody::gravity_scale`](crate::RigidBody::gravity_scale)).
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PhysicsSettings {
    /// Global gravity acceleration applied to dynamic bodies.
    pub gravity: Vec3,
}

impl Default for PhysicsSettings {
    fn default() -> Self {
        Self {
            gravity: DEFAULT_GRAVITY,
        }
    }
}

/// The physics module. Add it after [`DefaultModules`] to give an app a
/// rapier-backed simulation.
///
/// [`DefaultModules`]: oxide_engine::app::DefaultModules
///
/// ```
/// use oxide_engine::app::App;
/// use oxide_physics::PhysicsModule;
///
/// let mut app = App::new();
/// app.add_module(PhysicsModule);
/// assert!(app.has_module("physics"));
/// assert!(app.types.is_registered("RigidBody"));
/// ```
pub struct PhysicsModule;

impl Module for PhysicsModule {
    fn name(&self) -> &'static str {
        "physics"
    }

    fn build(&self, app: &mut App) {
        // Register the component types for reflection so they round-trip through
        // RON (scripts/AI) and are captured by the play-mode scene snapshot.
        app.register_type::<RigidBody>("RigidBody");
        app.register_type::<Collider>("Collider");
        app.register_type::<CharacterController>("CharacterController");

        // The global gravity setting; the simulation reads it each fixed step.
        app.insert_resource(PhysicsSettings::default());

        // The transient rapier world (rebuilt from components) and the
        // fixed-timestep system that syncs, steps, and writes poses back.
        app.insert_resource(PhysicsWorld::new());
        app.add_system(Schedule::FixedUpdate, step_physics);
    }
}

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

    #[test]
    fn module_registers_component_types() {
        let mut app = App::new();
        app.add_modules(DefaultModules);
        app.add_module(PhysicsModule);

        assert!(app.has_module("physics"));
        assert!(app.types.is_registered("RigidBody"));
        assert!(app.types.is_registered("Collider"));
        assert_eq!(
            app.get_resource::<PhysicsSettings>().map(|s| s.gravity),
            Some(DEFAULT_GRAVITY)
        );
    }

    #[test]
    fn removing_the_module_drops_its_types() {
        let mut app = App::new();
        app.add_module(PhysicsModule);
        assert!(app.types.is_registered("Collider"));

        assert!(app.remove_module("physics"));
        assert!(!app.has_module("physics"));
        assert!(!app.types.is_registered("RigidBody"));
        assert!(!app.types.is_registered("Collider"));
    }
}
