//! The [`Collider`] component and its [`ColliderShape`].
//!
//! A collider is the *shape* half of a physics object: the geometry the
//! simulation tests for contact, plus the material (friction/restitution) and
//! the [`LayerMask`] filtering that decides *what it collides with*. Paired with
//! a [`RigidBody`](crate::RigidBody) it becomes a moving body; on its own it is
//! static world geometry.
//!
//! The shape is modelled as a flat selector + dimension fields rather than a
//! data-carrying enum, mirroring [`MeshRenderer`]'s `PrimitiveShape` — that
//! keeps every field an individually-reflectable scalar so the inspector renders
//! a clean combo + drag-values with no per-type editor code. Convex-hull and
//! triangle-mesh colliders (which need mesh data) are a later Stage-9 piece.
//!
//! [`MeshRenderer`]: oxide_engine::render::MeshRenderer

use oxide_engine::layer::LayerMask;
use oxide_engine::math::Vec3;
use oxide_engine::reflect::{Reflect, ReflectEnum};
use serde::{Deserialize, Serialize};

/// The primitive shape of a [`Collider`].
///
/// Which dimension fields on the [`Collider`] are read depends on the shape:
/// - [`Box`](Self::Box) — `half_extents` (per-axis half sizes).
/// - [`Sphere`](Self::Sphere) — `radius`.
/// - [`Capsule`](Self::Capsule) — `radius` + `half_height` (the cylindrical
///   segment half-length, capped by hemispheres; axis is local `+Y`).
/// - [`Cylinder`](Self::Cylinder) — `radius` + `half_height` (axis is local
///   `+Y`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ReflectEnum)]
pub enum ColliderShape {
    /// Axis-aligned box; uses `half_extents`.
    #[default]
    Box,
    /// Sphere; uses `radius`.
    Sphere,
    /// Capsule along local `+Y`; uses `radius` + `half_height`.
    Capsule,
    /// Cylinder along local `+Y`; uses `radius` + `half_height`.
    Cylinder,
}

impl ColliderShape {
    /// Every shape, for editor menus.
    pub const ALL: [ColliderShape; 4] = [
        ColliderShape::Box,
        ColliderShape::Sphere,
        ColliderShape::Capsule,
        ColliderShape::Cylinder,
    ];

    /// A human-readable label.
    pub fn label(self) -> &'static str {
        match self {
            ColliderShape::Box => "Box",
            ColliderShape::Sphere => "Sphere",
            ColliderShape::Capsule => "Capsule",
            ColliderShape::Cylinder => "Cylinder",
        }
    }
}

/// Component: the collision shape and material of a physics object.
///
/// Attach on its own for static world geometry, or alongside a
/// [`RigidBody`](crate::RigidBody) for a moving body. `membership`/`filter` are
/// the engine's shared [`LayerMask`] primitive (Stage 5), so collision groups,
/// triggers, and scene queries all filter the same way.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
pub struct Collider {
    /// The primitive shape; selects which dimension fields below are used.
    pub shape: ColliderShape,
    /// Half-extents for [`ColliderShape::Box`] (per-axis half sizes).
    pub half_extents: Vec3,
    /// Radius for sphere / capsule / cylinder shapes.
    pub radius: f32,
    /// Half-height for capsule / cylinder shapes (along local `+Y`).
    pub half_height: f32,
    /// Coulomb friction coefficient (`0` = frictionless, ~`0.5` typical).
    pub friction: f32,
    /// Bounciness, `0` = inelastic, `1` = fully elastic.
    pub restitution: f32,
    /// Mass density (kg/m³-ish); used to derive a dynamic body's mass when its
    /// [`mass`](crate::RigidBody::mass) is `0`.
    pub density: f32,
    /// When `true` the collider is a **sensor (trigger)**: it reports overlap
    /// (enter/stay/exit events) without resolving contact, so things pass
    /// through it.
    pub sensor: bool,
    /// The layers this collider **belongs to** — what it *is*, for the other
    /// side's filter to select.
    pub membership: LayerMask,
    /// The layers this collider **collides with** — what it *cares about*. Two
    /// colliders interact only when each one's membership intersects the
    /// other's filter.
    pub filter: LayerMask,
}

impl Default for Collider {
    fn default() -> Self {
        Self {
            shape: ColliderShape::Box,
            // A unit cube (matches the default MeshRenderer primitive).
            half_extents: Vec3::splat(0.5),
            radius: 0.5,
            half_height: 0.5,
            friction: 0.5,
            restitution: 0.0,
            density: 1.0,
            sensor: false,
            // Belong to layer 0 ("Default") and collide with everything, so a
            // freshly-added collider interacts out of the box.
            membership: LayerMask::layer(0),
            filter: LayerMask::ALL,
        }
    }
}

impl Collider {
    /// A box collider with the given per-axis half-extents.
    pub fn cuboid(half_extents: Vec3) -> Self {
        Self {
            shape: ColliderShape::Box,
            half_extents,
            ..Self::default()
        }
    }

    /// A sphere collider of the given radius.
    pub fn ball(radius: f32) -> Self {
        Self {
            shape: ColliderShape::Sphere,
            radius,
            ..Self::default()
        }
    }

    /// A capsule collider (radius + cylindrical half-height along `+Y`).
    pub fn capsule(radius: f32, half_height: f32) -> Self {
        Self {
            shape: ColliderShape::Capsule,
            radius,
            half_height,
            ..Self::default()
        }
    }

    /// This collider made a sensor (trigger).
    pub fn as_sensor(mut self) -> Self {
        self.sensor = true;
        self
    }

    /// This collider on the given membership/filter masks.
    pub fn with_layers(mut self, membership: LayerMask, filter: LayerMask) -> Self {
        self.membership = membership;
        self.filter = filter;
        self
    }
}

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

    #[test]
    fn default_is_a_unit_box_that_collides_with_everything() {
        let c = Collider::default();
        assert_eq!(c.shape, ColliderShape::Box);
        assert_eq!(c.half_extents, Vec3::splat(0.5));
        assert!(!c.sensor);
        assert_eq!(c.membership, LayerMask::layer(0));
        assert_eq!(c.filter, LayerMask::ALL);
    }

    #[test]
    fn constructors_pick_the_shape() {
        assert_eq!(Collider::ball(2.0).shape, ColliderShape::Sphere);
        assert_eq!(Collider::ball(2.0).radius, 2.0);
        assert_eq!(Collider::capsule(0.3, 0.8).shape, ColliderShape::Capsule);
        assert!(Collider::default().as_sensor().sensor);
    }

    #[test]
    fn with_layers_sets_masks() {
        let c = Collider::default().with_layers(LayerMask::layer(2), LayerMask::layer(3));
        assert_eq!(c.membership, LayerMask::layer(2));
        assert_eq!(c.filter, LayerMask::layer(3));
    }

    #[test]
    fn round_trips_through_ron() {
        let c = Collider::capsule(0.4, 1.0).as_sensor().with_layers(
            LayerMask::layer(1),
            LayerMask::layer(2).union(LayerMask::layer(5)),
        );
        let ron = ron::to_string(&c).unwrap();
        let back: Collider = ron::from_str(&ron).unwrap();
        assert_eq!(c, back);
    }

    #[test]
    fn shape_variants_are_reflected_for_a_combo() {
        assert_eq!(
            ColliderShape::variants(),
            &["Box", "Sphere", "Capsule", "Cylinder"]
        );
    }
}
