// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// An infinite plane in space: the set of points `p` with `dot(normal, p) ==
/// distance`, where the normal is a unit vector.
///
/// The offset form rather than point-and-normal, because the signed distance of
/// a point is then a dot product and no subtraction, which is what a frustum
/// does six times per box.
///
/// Values are immutable.
#derive(Eq)
pub(all) struct Plane {
  /// The unit normal: which side is out.
  normal : Vec3
  /// How far the plane is from the origin, measured along the normal.
  distance : Float
}

///|
pub extend Plane with Eq::{equal, not_equal}

///|
/// The plane whose normal is `normal` and whose signed distance from the origin
/// is `distance`.
///
/// `normal` is normalized, and `distance` scaled with it, so that
/// `signed_distance` counts the same units as the coordinates. A zero normal
/// names no plane and aborts: there is no answer to give back.
pub fn Plane::new(normal : Vec3, distance : Float) -> Plane {
  match normal.try_normalize() {
    None => abort("Plane::new with a zero normal")
    Some(unit) => { normal: unit, distance: distance / normal.length(), }
  }
}

///|
/// The plane through `point` with the given normal.
pub fn Plane::from_point_normal(point : Vec3, normal : Vec3) -> Plane? {
  match normal.try_normalize() {
    None => None
    Some(unit) => Some({ normal: unit, distance: unit.dot(point), })
  }
}

///|
/// The plane through the three points, or `None` when they are collinear.
///
/// The normal faces the way the points wind, by the right-hand rule, so the
/// winding order decides which side is positive.
pub fn Plane::from_points(a : Vec3, b : Vec3, c : Vec3) -> Plane? {
  match (b - a).cross(c - a).try_normalize() {
    None => None
    Some(normal) => Some({ normal, distance: normal.dot(a), })
  }
}

///|
/// The distance from the plane to the point, positive on the side the normal
/// points to.
pub fn Plane::signed_distance(self : Plane, point : Vec3) -> Float {
  self.normal.dot(point) - self.distance
}

///|
/// The point of the plane closest to `point`: the point projected onto it.
pub fn Plane::project(self : Plane, point : Vec3) -> Vec3 {
  point - self.normal.scale(self.signed_distance(point))
}

///|
/// Where the ray meets the plane, as a `t`, or `None` when it runs parallel or
/// the crossing is behind the origin.
pub fn Plane::intersect_ray(self : Plane, ray : Ray3) -> Float? {
  let denominator = self.normal.dot(ray.direction)
  if denominator == 0.0 {
    return None
  }
  let t = (self.distance - self.normal.dot(ray.origin)) / denominator
  if t >= 0.0 {
    Some(t)
  } else {
    None
  }
}

///|
/// The same plane with the other side facing out.
pub fn Plane::flip(self : Plane) -> Plane {
  { normal: -self.normal, distance: -self.distance, }
}

///|
/// Whether the normal and the distance are finite.
pub fn Plane::is_finite(self : Plane) -> Bool {
  self.normal.is_finite() && !(self.distance.is_nan() || self.distance.is_inf())
}

///|
/// Whether the two planes are the same to within `eps`.
pub fn Plane::approx(self : Plane, other : Plane, eps? : Float) -> Bool {
  self.normal.approx(other.normal, eps?) &&
  approx(self.distance, other.distance, eps?)
}

///|
pub impl Show for Plane with fn output(self, logger) {
  logger.write_string("Plane(\{self.normal}, \{self.distance})")
}

///|
pub extend Plane with Show::{output, to_string}

///|
pub impl Debug for Plane with fn to_repr(self) {
  @debug.Repr::ctor("Plane", [
    (Some("normal"), self.normal.to_repr()),
    (Some("distance"), @debug.Repr::float(self.distance)),
  ])
}

///|
pub extend Plane with Debug::{to_repr}

///|
/// A sphere: a centre and a radius.
///
/// Values are immutable.
#derive(Eq)
pub(all) struct Sphere {
  /// The centre.
  center : Vec3
  /// The radius.
  radius : Float
}

///|
pub extend Sphere with Eq::{equal, not_equal}

///|
/// The sphere with the given centre and radius.
pub fn Sphere::new(center : Vec3, radius : Float) -> Sphere {
  { center, radius, }
}

///|
/// Whether the point is inside or on the surface.
pub fn Sphere::contains_point(self : Sphere, point : Vec3) -> Bool {
  point.distance_squared(self.center) <= self.radius * self.radius
}

///|
/// Whether the two spheres overlap or touch.
pub fn Sphere::intersects_sphere(self : Sphere, other : Sphere) -> Bool {
  let reach = self.radius + other.radius
  self.center.distance_squared(other.center) <= reach * reach
}

///|
/// Whether the sphere and the box overlap or touch.
///
/// The point of the box closest to the centre is the one that decides it: if
/// that point is within the radius, the two meet. Testing the centre against
/// the box alone would call a sphere near a corner a miss.
pub fn Sphere::intersects_aabb(self : Sphere, box : Aabb3) -> Bool {
  let closest = self.center.clamp(box.min, box.max)
  closest.distance_squared(self.center) <= self.radius * self.radius
}

///|
/// The area of the surface.
pub fn Sphere::surface_area(self : Sphere) -> Float {
  let four : Float = 4.0
  four * pi * self.radius * self.radius
}

///|
/// The volume it encloses.
pub fn Sphere::volume(self : Sphere) -> Float {
  let four_thirds : Float = 4.0 / 3.0
  four_thirds * pi * self.radius * self.radius * self.radius
}

///|
/// Whether the two spheres agree to within `eps`.
pub fn Sphere::approx(self : Sphere, other : Sphere, eps? : Float) -> Bool {
  self.center.approx(other.center, eps?) &&
  approx(self.radius, other.radius, eps?)
}

///|
pub impl Show for Sphere with fn output(self, logger) {
  logger.write_string("Sphere(\{self.center}, \{self.radius})")
}

///|
pub extend Sphere with Show::{output, to_string}

///|
pub impl Debug for Sphere with fn to_repr(self) {
  @debug.Repr::ctor("Sphere", [
    (Some("center"), self.center.to_repr()),
    (Some("radius"), @debug.Repr::float(self.radius)),
  ])
}

///|
pub extend Sphere with Debug::{to_repr}

///|
/// A triangle in space: three corners in winding order.
///
/// The winding decides which side the normal faces, by the right-hand rule.
///
/// Values are immutable.
#derive(Eq)
pub(all) struct Triangle {
  /// The first corner.
  a : Vec3
  /// The second corner.
  b : Vec3
  /// The third corner.
  c : Vec3
}

///|
pub extend Triangle with Eq::{equal, not_equal}

///|
/// The triangle with the three given corners.
pub fn Triangle::new(a : Vec3, b : Vec3, c : Vec3) -> Triangle {
  { a, b, c, }
}

///|
/// The unit normal, facing the way the corners wind.
///
/// A triangle with no area — three collinear points — has no normal, and comes
/// back as the zero vector rather than a NaN.
pub fn Triangle::normal(self : Triangle) -> Vec3 {
  (self.b - self.a).cross(self.c - self.a).normalize()
}

///|
/// The area.
pub fn Triangle::area(self : Triangle) -> Float {
  let half : Float = 0.5
  half * (self.b - self.a).cross(self.c - self.a).length()
}

///|
/// The barycentric coordinates of a point with respect to the triangle.
///
/// The three weights sum to one, and a point inside has all three in `0..1`. The
/// point is projected onto the triangle's plane first, so a point off the plane
/// still has coordinates — they are those of the closest point on the plane.
/// `None` when the triangle has no area to weight against.
pub fn Triangle::barycentric(self : Triangle, point : Vec3) -> Vec3? {
  let v0 = self.b - self.a
  let v1 = self.c - self.a
  let v2 = point - self.a
  let d00 = v0.dot(v0)
  let d01 = v0.dot(v1)
  let d11 = v1.dot(v1)
  let d20 = v2.dot(v0)
  let d21 = v2.dot(v1)
  let denominator = d00 * d11 - d01 * d01
  if denominator == 0.0 {
    None
  } else {
    let inv : Float = 1.0 / denominator
    let v = (d11 * d20 - d01 * d21) * inv
    let w = (d00 * d21 - d01 * d20) * inv
    let one : Float = 1.0
    Some({ x: one - v - w, y: v, z: w, })
  }
}

///|
/// Where the ray meets the triangle, as a `t`, or `None` when it misses.
///
/// The same test as `Ray3::intersect_triangle`, written from this side for code
/// that already has the triangle.
pub fn Triangle::intersect_ray(self : Triangle, ray : Ray3) -> Float? {
  ray.intersect_triangle(self)
}

///|
/// Whether the two triangles have the same corners to within `eps`.
pub fn Triangle::approx(
  self : Triangle,
  other : Triangle,
  eps? : Float,
) -> Bool {
  self.a.approx(other.a, eps?) &&
  self.b.approx(other.b, eps?) &&
  self.c.approx(other.c, eps?)
}

///|
pub impl Show for Triangle with fn output(self, logger) {
  logger.write_string("Triangle(\{self.a}, \{self.b}, \{self.c})")
}

///|
pub extend Triangle with Show::{output, to_string}

///|
pub impl Debug for Triangle with fn to_repr(self) {
  @debug.Repr::ctor("Triangle", [
    (Some("a"), self.a.to_repr()),
    (Some("b"), self.b.to_repr()),
    (Some("c"), self.c.to_repr()),
  ])
}

///|
pub extend Triangle with Debug::{to_repr}