//! An infinite [`Plane`] in Hessian normal form.

use crate::math::Ray;
use glam::Vec3;
use serde::{Deserialize, Serialize};

/// An infinite plane defined by a unit `normal` and a signed distance `d` from
/// the origin, such that every point `p` on the plane satisfies
/// `normal · p + d = 0`.
///
/// The positive half-space is the side the normal points toward.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Plane {
    /// Unit-length plane normal.
    pub normal: Vec3,
    /// Signed distance from the origin along `-normal`.
    pub d: f32,
}

impl Plane {
    /// Creates a plane from a normal and signed distance, normalizing the input
    /// (and scaling `d` to match) so the result is in Hessian normal form.
    #[inline]
    pub fn new(normal: Vec3, d: f32) -> Self {
        let len = normal.length();
        if len > 0.0 {
            Self {
                normal: normal / len,
                d: d / len,
            }
        } else {
            Self { normal, d }
        }
    }

    /// Creates a plane from a point on it and a normal direction.
    #[inline]
    pub fn from_point_normal(point: Vec3, normal: Vec3) -> Self {
        let n = normal.normalize_or_zero();
        Self {
            normal: n,
            d: -n.dot(point),
        }
    }

    /// Creates a plane through three points. Winding `a → b → c` determines the
    /// normal direction (right-hand rule).
    #[inline]
    pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> Self {
        let normal = (b - a).cross(c - a);
        Self::from_point_normal(a, normal)
    }

    /// The signed distance from `point` to the plane. Positive on the side the
    /// normal points toward, negative behind it, zero on the plane.
    #[inline]
    pub fn signed_distance(&self, point: Vec3) -> f32 {
        self.normal.dot(point) + self.d
    }

    /// Projects `point` orthogonally onto the plane.
    #[inline]
    pub fn project_point(&self, point: Vec3) -> Vec3 {
        point - self.normal * self.signed_distance(point)
    }

    /// Returns the intersection distance `t` along `ray`, or `None` if the ray
    /// is parallel to the plane (or points away from it).
    pub fn ray_intersection(&self, ray: &Ray) -> Option<f32> {
        let denom = self.normal.dot(ray.direction);
        if denom.abs() <= f32::EPSILON {
            return None; // Parallel.
        }
        let t = -(self.normal.dot(ray.origin) + self.d) / denom;
        (t >= 0.0).then_some(t)
    }

    /// Returns a plane facing the opposite direction (same geometric plane).
    #[inline]
    pub fn flipped(&self) -> Plane {
        Plane {
            normal: -self.normal,
            d: -self.d,
        }
    }
}

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

    const EPS: f32 = 1e-4;

    #[test]
    fn from_point_normal_passes_through_point() {
        let p = Plane::from_point_normal(Vec3::new(0.0, 2.0, 0.0), Vec3::Y);
        assert!(p.signed_distance(Vec3::new(5.0, 2.0, -3.0)).abs() <= EPS);
        assert!((p.signed_distance(Vec3::new(0.0, 5.0, 0.0)) - 3.0).abs() <= EPS);
        assert!((p.signed_distance(Vec3::ZERO) + 2.0).abs() <= EPS);
    }

    #[test]
    fn new_normalizes() {
        let p = Plane::new(Vec3::new(0.0, 0.0, 4.0), 8.0);
        assert!((p.normal - Vec3::Z).length() <= EPS);
        assert!((p.d - 2.0).abs() <= EPS);
    }

    #[test]
    fn from_points_winding() {
        let p = Plane::from_points(Vec3::ZERO, Vec3::X, Vec3::Y);
        // X cross Y = Z.
        assert!((p.normal - Vec3::Z).length() <= EPS);
        assert!(p.d.abs() <= EPS);
    }

    #[test]
    fn project_lands_on_plane() {
        let p = Plane::from_point_normal(Vec3::ZERO, Vec3::Y);
        let proj = p.project_point(Vec3::new(3.0, 7.0, -2.0));
        assert!((proj - Vec3::new(3.0, 0.0, -2.0)).length() <= EPS);
        assert!(p.signed_distance(proj).abs() <= EPS);
    }

    #[test]
    fn ray_intersects_plane() {
        let p = Plane::from_point_normal(Vec3::ZERO, Vec3::Y);
        let ray = Ray::new(Vec3::new(0.0, 5.0, 0.0), Vec3::NEG_Y);
        let t = p.ray_intersection(&ray).expect("should hit");
        assert!((t - 5.0).abs() <= EPS);
    }

    #[test]
    fn ray_parallel_misses() {
        let p = Plane::from_point_normal(Vec3::ZERO, Vec3::Y);
        let ray = Ray::new(Vec3::new(0.0, 5.0, 0.0), Vec3::X);
        assert_eq!(p.ray_intersection(&ray), None);
    }

    #[test]
    fn ray_pointing_away_misses() {
        let p = Plane::from_point_normal(Vec3::ZERO, Vec3::Y);
        let ray = Ray::new(Vec3::new(0.0, 5.0, 0.0), Vec3::Y);
        assert_eq!(p.ray_intersection(&ray), None);
    }

    #[test]
    fn flipped_reverses_sign() {
        let p = Plane::from_point_normal(Vec3::new(0.0, 2.0, 0.0), Vec3::Y);
        let f = p.flipped();
        let pt = Vec3::new(0.0, 5.0, 0.0);
        assert!((p.signed_distance(pt) + f.signed_distance(pt)).abs() <= EPS);
    }
}
