#![deny(
    deprecated_in_future,
    exported_private_dependencies,
    future_incompatible,
    missing_copy_implementations,
    missing_crate_level_docs,
    missing_debug_implementations,
    missing_docs,
    private_in_public,
    rust_2018_compatibility,
    rust_2018_idioms, // this complains about elided lifetimes.
    trivial_casts,
    trivial_numeric_casts,
    unsafe_code,
    unstable_features,
    unused_import_braces,
    unused_qualifications
)]
/*!
A simple utility for working with lines.

Provides a method to get the distance from a point to the `Line`.

*/

use mint::Point2;
use num_traits::{cast::NumCast, ToPrimitive};

/// Calculates the distance between 2 points.
trait Distance {
    fn distance(&self, other: &Self) -> f64;
}

fn cast<T: ToPrimitive>(point: Point2<T>) -> Point2<f64> {
    let Point2 { x, y } = point;
    Point2 {
        x: NumCast::from(x).unwrap(),
        y: NumCast::from(y).unwrap(),
    }
}

impl<T> Distance for Point2<T>
where
    T: NumCast + Copy,
{
    fn distance(&self, other: &Self) -> f64 {
        let a = cast(*self);
        let b = cast(*other);

        (a.y - b.y).hypot(a.x - b.x)
    }
}

/// A line defined by 2 points.
#[derive(Copy, Clone, PartialEq, Debug)]
/// A line defined by 2 points.
pub struct Line<T> {
    a: Point2<T>,
    b: Point2<T>,
}

impl<T> Line<T> {
    /// Creates a new `Line` given points `a` and `b`.
    ///
    /// Will panic if the points are the same.
    pub const fn new(a: Point2<T>, b: Point2<T>) -> Self {
        Self { a, b }
    }
}

impl<T> Line<T>
where
    T: NumCast + Copy,
{
    /// Returns a `LineDistance` based on `self`.
    pub fn distance(&self) -> LineDistance {
        LineDistance::new(self.a, self.b)
    }
}

/// Computes the distance to a line.
///
/// Can be used to calculate the distance to many points.
/// Pre-calculates a bit of the algorithm for speed.
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct LineDistance {
    a: f64,
    b: f64,
    c: f64,
    /// The distance between 2 points
    pub length: f64,
}

impl LineDistance {
    /// Creates a new LineDistance
    pub fn new<T>(p1: Point2<T>, p2: Point2<T>) -> Self
    where
        T: NumCast + Copy,
    {
        let Point2 { x: x1, y: y1, .. } = cast(p1);
        let Point2 { x: x2, y: y2, .. } = cast(p2);

        let a = y2 - y1;
        let b = x2 - x1;
        let c = (x2 * y1) - (y2 * x1);
        let length = p1.distance(&p2);

        Self { a, b, c, length }
    }

    /// Returns the perpendicular distance from the line to the point.
    ///
    /// None is returned if the line has no length.
    pub fn to<T>(&self, point: &Point2<T>) -> Option<f64>
    where
        T: NumCast + Copy,
    {
        let Self { a, b, c, length } = self;

        if 0.0 == *length {
            None
        } else {
            let point = cast(*point);
            // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_two_points
            Some(((a * point.x) - (b * point.y) + c).abs() / length)
        }
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    fn approx_eq<T: num_traits::Float>(a: T, b: T) -> bool {
        num_traits::Float::abs(a - b) < num_traits::Float::epsilon()
    }

    #[test]
    fn same_points() {
        type Point = Point2<i32>;

        let a = Point { x: 3, y: -3 };
        let b = Point { x: 3, y: -3 };

        let line = Line::new(a, b);
        let line_distance = line.distance();

        assert!(line_distance.to(&Point { x: 0, y: 0 }).is_none());
    }

    #[test]
    fn distance_simple() {
        type Point = Point2<i32>;

        let a = Point { x: -3, y: 1 };
        let b = Point { x: 3, y: 1 };
        let line = Line::new(a, b);
        let line_distance = line.distance();

        assert!(approx_eq(
            5.0,
            line_distance.to(&Point { x: -50, y: 6 }).unwrap()
        ));
        assert!(approx_eq(
            5.0,
            line_distance.to(&Point { x: 10, y: -4 }).unwrap()
        ));

        assert!(approx_eq(
            5.0,
            line_distance
                .to(&Point2::<f32> { x: 32.0, y: -4.0 })
                .unwrap()
        ));
    }

    #[test]
    fn distance_angle() {
        type Point = Point2<i32>;

        let a = Point { x: -3, y: -3 };
        let b = Point { x: 3, y: 3 };
        let line = Line::new(a, b);
        let line_distance = line.distance();

        assert!(approx_eq(
            dbg!(line_distance.to(&Point { x: -3, y: 3 }).unwrap()),
            4.242640687119286
        ));

        assert!(approx_eq(
            line_distance.to(&Point { x: 3, y: -3 }).unwrap(),
            4.242640687119286
        ));
    }
}
