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

///|
/// An infinite line in space: a point and a direction.
///
/// A ray has a start and runs one way; a line has neither end, which is what a
/// construction axis, a normal or the intersection of two planes is. The
/// direction need not be a unit vector — the results are the same whatever its
/// length — but a zero direction names no line, and every operation treats it as
/// the single point `point`.
///
/// Values are immutable.
#derive(Eq)
pub(all) struct Line3 {
  /// A point on the line.
  point : Vec3
  /// The direction it runs in; its length does not matter.
  direction : Vec3
}

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

///|
/// The line through `point` in `direction`.
pub fn Line3::new(point : Vec3, direction : Vec3) -> Line3 {
  { point, direction, }
}

///|
/// The point `t` of the direction from `point`; `t` may be any number.
pub fn Line3::at(self : Line3, t : Float) -> Vec3 {
  self.point + self.direction.scale(t)
}

///|
/// The point of the line closest to `other`.
///
/// The perpendicular foot: the vector from it to `other` is perpendicular to the
/// direction. A zero direction leaves the line a point, and the answer is that
/// point.
pub fn Line3::closest_point_to(self : Line3, other : Vec3) -> Vec3 {
  let along = self.direction.length_squared()
  if along == 0.0 {
    self.point
  } else {
    self.at((other - self.point).dot(self.direction) / along)
  }
}

///|
/// The distance from the line to the point.
pub fn Line3::distance_to_point(self : Line3, other : Vec3) -> Float {
  other.distance(self.closest_point_to(other))
}

///|
/// The point of this line closest to the other line.
///
/// For two lines that meet or are skew, the closest points are the pair joined by
/// the common perpendicular; this is the one on this line. Parallel lines have a
/// whole family of closest pairs and the foot from the other line's point is
/// returned. A degenerate line — a zero direction — gives its point.
pub fn Line3::closest_point_to_line(self : Line3, other : Line3) -> Vec3 {
  let d1 = self.direction
  let d2 = other.direction
  let a = d1.dot(d1)
  if a == 0.0 {
    return self.point
  }
  let b = d1.dot(d2)
  let c = d2.dot(d2)
  let w = self.point - other.point
  let d = d1.dot(w)
  let e = d2.dot(w)
  let denominator = a * c - b * b
  if denominator == 0.0 {
    // Parallel, or the other line is a point: the foot from its point.
    self.closest_point_to(other.point)
  } else {
    self.at((b * e - c * d) / denominator)
  }
}

///|
/// The distance between the two lines: zero when they meet.
pub fn Line3::distance_to_line(self : Line3, other : Line3) -> Float {
  let cross = self.direction.cross(other.direction)
  let area = cross.length_squared()
  if area == 0.0 {
    // Parallel, or one of them degenerate: the distance to the other's point
    // is the distance between them.
    if self.direction.length_squared() == 0.0 {
      other.distance_to_point(self.point)
    } else {
      self.distance_to_point(other.point)
    }
  } else {
    let w = other.point - self.point
    w.dot(cross).abs() / cross.length()
  }
}

///|
/// Where the line cuts the plane, or `None` when it runs parallel to it.
///
/// A line has no ends, so any crossing counts, on either side of `point`.
pub fn Line3::intersect_plane(self : Line3, plane : Plane) -> Vec3? {
  let denominator = plane.normal.dot(self.direction)
  if denominator == 0.0 {
    None
  } else {
    let t = (plane.distance - plane.normal.dot(self.point)) / denominator
    Some(self.at(t))
  }
}

///|
/// Whether the point and the direction are finite.
pub fn Line3::is_finite(self : Line3) -> Bool {
  self.point.is_finite() && self.direction.is_finite()
}

///|
/// Whether the two lines have the same point and direction to within `eps`.
pub fn Line3::approx(self : Line3, other : Line3, eps? : Float) -> Bool {
  self.point.approx(other.point, eps?) &&
  self.direction.approx(other.direction, eps?)
}

///|
pub impl Show for Line3 with fn output(self, logger) {
  logger.write_string("Line3(\{self.point}, \{self.direction})")
}

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

///|
pub impl Debug for Line3 with fn to_repr(self) {
  @debug.Repr::ctor("Line3", [
    (Some("point"), self.point.to_repr()),
    (Some("direction"), self.direction.to_repr()),
  ])
}

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

///|
/// Whether the infinite line meets the frustum at all.
///
/// Each of the six planes is a linear inequality in the line's parameter, so the
/// part of the line inside the frustum is the intersection of six half-lines:
/// one interval. The line meets the frustum exactly when that interval is not
/// empty, which is the honest test an infinite line needs — unlike a ray, it has
/// no start that could already be outside one plane while inside another.
pub fn Frustum::intersects_line(self : Frustum, line : Line3) -> Bool {
  let mut low : Float = -3.4028234663852886e38
  let mut high : Float = 3.4028234663852886e38
  for i in 0..<6 {
    let plane = self.planes[i]
    let slope = plane.normal.dot(line.direction)
    let offset = plane.signed_distance(line.point)
    if slope == 0.0 {
      if offset < 0.0 {
        return false
      }
    } else {
      let crossing = -offset / slope
      if slope > 0.0 {
        if crossing > low {
          low = crossing
        }
      } else if crossing < high {
        high = crossing
      }
      if low > high {
        return false
      }
    }
  }
  true
}