use crate::float::compatible::FloatPointCompatible;
use crate::float::number::FloatNumber;
use core::fmt;

#[derive(Debug, Clone)]
pub struct FloatRect<T: FloatNumber> {
    pub min_x: T,
    pub max_x: T,
    pub min_y: T,
    pub max_y: T,
}

impl<T: FloatNumber> FloatRect<T> {
    #[inline(always)]
    pub fn width(&self) -> T {
        self.max_x - self.min_x
    }

    #[inline(always)]
    pub fn height(&self) -> T {
        self.max_y - self.min_y
    }

    #[inline(always)]
    pub fn new(min_x: T, max_x: T, min_y: T, max_y: T) -> Self {
        Self {
            min_x,
            max_x,
            min_y,
            max_y,
        }
    }

    #[inline(always)]
    pub fn zero() -> Self {
        let zero = FloatNumber::from_float(0.0);
        Self {
            min_x: zero,
            max_x: zero,
            min_y: zero,
            max_y: zero,
        }
    }

    #[inline]
    pub fn with_point<P: FloatPointCompatible<Scalar = T>>(point: P) -> Self {
        Self {
            min_x: point.x(),
            max_x: point.x(),
            min_y: point.y(),
            max_y: point.y(),
        }
    }

    #[inline]
    pub fn with_points<P>(points: &[P]) -> Option<Self>
    where
        P: FloatPointCompatible<Scalar = T>,
    {
        Self::with_iter(points.iter())
    }

    #[inline]
    pub fn with_iter<'a, I, P>(iter: I) -> Option<Self>
    where
        I: Iterator<Item = &'a P>,
        P: FloatPointCompatible<Scalar = T> + 'a,
        T: FloatNumber,
    {
        let mut iter = iter;
        let first_point = iter.next()?;
        let mut rect = Self {
            min_x: first_point.x(),
            max_x: first_point.x(),
            min_y: first_point.y(),
            max_y: first_point.y(),
        };

        for p in iter {
            rect.unsafe_add_point(p);
        }

        Some(rect)
    }

    #[inline]
    pub fn with_rects(rect_0: &Self, rect_1: &Self) -> Self {
        let min_x = rect_0.min_x.min(rect_1.min_x);
        let max_x = rect_0.max_x.max(rect_1.max_x);
        let min_y = rect_0.min_y.min(rect_1.min_y);
        let max_y = rect_0.max_y.max(rect_1.max_y);
        FloatRect::new(min_x, max_x, min_y, max_y)
    }

    #[inline]
    pub fn with_optional_rects(rect_0: Option<&Self>, rect_1: Option<&Self>) -> Option<Self> {
        match (rect_0, rect_1) {
            (Some(r0), Some(r1)) => Some(Self::with_rects(r0, r1)),
            (Some(r0), None) => Some(r0.clone()),
            (None, Some(r1)) => Some(r1.clone()),
            (None, None) => None,
        }
    }

    #[inline]
    pub fn add_point<P: FloatPointCompatible<Scalar = T>>(&mut self, point: &P) {
        if self.min_x > point.x() {
            self.min_x = point.x()
        }
        if self.max_x < point.x() {
            self.max_x = point.x()
        }

        if self.min_y > point.y() {
            self.min_y = point.y()
        }
        if self.max_y < point.y() {
            self.max_y = point.y()
        }
    }

    #[inline]
    pub fn add_offset(&mut self, offset: T) {
        self.max_x = self.max_x + offset;
        self.max_y = self.max_y + offset;
        self.min_x = self.min_x - offset;
        self.min_y = self.min_y - offset;
    }

    #[inline]
    pub fn unsafe_add_point<P: FloatPointCompatible<Scalar = T>>(&mut self, point: &P) {
        if self.min_x > point.x() {
            self.min_x = point.x()
        } else if self.max_x < point.x() {
            self.max_x = point.x()
        }

        if self.min_y > point.y() {
            self.min_y = point.y()
        } else if self.max_y < point.y() {
            self.max_y = point.y()
        }
    }

    #[inline(always)]
    pub fn contains<P: FloatPointCompatible<Scalar = T>>(&self, point: &P) -> bool {
        self.min_x <= point.x()
            && point.x() <= self.max_x
            && self.min_y <= point.y()
            && point.y() <= self.max_y
    }

    #[inline(always)]
    pub fn contains_with_radius<P: FloatPointCompatible<Scalar = T>>(&self, point: &P, radius: T) -> bool {
        let min_x = self.min_x - radius;
        let max_x = self.max_x + radius;
        let min_y = self.min_y - radius;
        let max_y = self.max_y + radius;
        min_x <= point.x() && point.x() <= max_x && min_y <= point.y() && point.y() <= max_y
    }
}

impl<T: FloatNumber> fmt::Display for FloatRect<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "({}, {})-({}, {})",
            self.min_x, self.min_y, self.max_x, self.max_y
        )
    }
}

#[cfg(test)]
mod tests {
    use crate::float::rect::FloatRect;

    #[test]
    fn test_0() {
        let points = [[-2.0, -4.0], [-2.0, 3.0], [5.0, 3.0], [5.0, -4.0]];

        let rect: FloatRect<f64> = FloatRect::with_iter(points.iter()).unwrap();

        assert_eq!((rect.max_x - 5.0).abs() < 0.000_0001, true);
        assert_eq!((rect.min_x + 2.0).abs() < 0.000_0001, true);
        assert_eq!((rect.max_y - 3.0).abs() < 0.000_0001, true);
        assert_eq!((rect.min_y + 4.0).abs() < 0.000_0001, true);
    }

    #[test]
    fn test_1() {
        let r0 = Some(&FloatRect::new(-2.0, 2.0, -2.0, 2.0));
        let r1 = Some(&FloatRect::new(-4.0, 4.0, -4.0, 4.0));
        let rr = FloatRect::with_optional_rects(r0, r1).unwrap();

        assert_eq!(-4.0, rr.min_x);
        assert_eq!(-4.0, rr.min_y);
        assert_eq!(4.0, rr.max_x);
        assert_eq!(4.0, rr.max_y);
        assert!(FloatRect::with_optional_rects(r0, None).is_some());
        assert!(FloatRect::with_optional_rects(None, r1).is_some());
        assert!(FloatRect::<f32>::with_optional_rects(None, None).is_none());
    }
}
