use crate::float::compatible::FloatPointCompatible;
use crate::float::number::FloatNumber;
use crate::float::rect::FloatRect;
use crate::int::point::IntPoint;

#[derive(Clone)]
pub struct FloatPointAdapter<P: FloatPointCompatible> {
    pub dir_scale: P::Scalar,
    pub inv_scale: P::Scalar,
    pub offset: P,
    pub rect: FloatRect<P::Scalar>,
}

impl<P: FloatPointCompatible> FloatPointAdapter<P> {
    #[inline]
    pub fn new(rect: FloatRect<P::Scalar>) -> Self {
        let a = rect.width() * FloatNumber::from_float(0.5);
        let b = rect.height() * FloatNumber::from_float(0.5);

        let x = rect.min_x + a;
        let y = rect.min_y + b;

        let offset = P::from_xy(x, y);

        let max = a.max(b);

        // degenerate case
        if max == FloatNumber::from_float(0.0) {
            return Self {
                dir_scale: FloatNumber::from_float(1.0),
                inv_scale: FloatNumber::from_float(1.0),
                offset,
                rect,
            };
        }

        let log2 = max.log2().to_i32();
        let ie = 29 - log2;
        let e = ie as f64;

        let dir_scale = FloatNumber::from_float(libm::exp2(e));
        let inv_scale = FloatNumber::from_float(libm::exp2(-e));

        Self {
            dir_scale,
            inv_scale,
            offset,
            rect,
        }
    }

    #[inline]
    pub fn with_scale(rect: FloatRect<P::Scalar>, scale: f64) -> Self {
        let a = rect.width() * FloatNumber::from_float(0.5);
        let b = rect.height() * FloatNumber::from_float(0.5);

        let x = rect.min_x + a;
        let y = rect.min_y + b;

        let offset = P::from_xy(x, y);

        let max = a.max(b);

        // degenerate case
        if max == FloatNumber::from_float(0.0) {
            return Self {
                dir_scale: FloatNumber::from_float(1.0),
                inv_scale: FloatNumber::from_float(1.0),
                offset,
                rect,
            };
        }

        let dir_scale = FloatNumber::from_float(scale);
        let inv_scale = FloatNumber::from_float(1.0 / scale);

        Self {
            dir_scale,
            inv_scale,
            offset,
            rect,
        }
    }

    #[inline]
    pub fn with_iter<'a, I>(iter: I) -> Self
    where
        I: Iterator<Item = &'a P>,
        P: 'a,
    {
        Self::new(FloatRect::with_iter(iter).unwrap_or(FloatRect::zero()))
    }

    #[inline(always)]
    pub fn int_to_float(&self, point: &IntPoint) -> P {
        let fx: P::Scalar = FloatNumber::from_i32(point.x);
        let fy: P::Scalar = FloatNumber::from_i32(point.y);
        let x = fx * self.inv_scale + self.offset.x();
        let y = fy * self.inv_scale + self.offset.y();
        let float = P::from_xy(x, y);

        if cfg!(debug_assertions) {
            let radius = self.rect.height().max(self.rect.width()) * P::Scalar::from_float(0.01);
            if !self.rect.contains_with_radius(&float, radius) {
                panic!(
                    "You are trying to convert a point[{}, {}] which is out of rect: {}",
                    x, y, self.rect
                );
            }
        }

        float
    }

    #[inline(always)]
    pub fn float_to_int(&self, point: &P) -> IntPoint {
        if cfg!(debug_assertions) {
            let radius = self.rect.height().max(self.rect.width()) * P::Scalar::from_float(0.01);
            if !self.rect.contains_with_radius(point, radius) {
                panic!(
                    "You are trying to convert a point[{}, {}] which is out of rect: {}",
                    point.x(),
                    point.y(),
                    self.rect
                );
            }
        }
        let x = ((point.x() - self.offset.x()) * self.dir_scale).to_i32();
        let y = ((point.y() - self.offset.y()) * self.dir_scale).to_i32();
        IntPoint { x, y }
    }

    #[inline(always)]
    pub fn sqr_float_to_int(&self, value: P::Scalar) -> u64 {
        let scale = self.dir_scale;
        let sqr_scale = scale * scale;
        (sqr_scale * value).to_f64() as u64
    }

    #[inline(always)]
    pub fn len_float_to_int(&self, value: P::Scalar) -> i32 {
        (self.dir_scale * value).to_f64() as i32
    }
}

#[cfg(test)]
mod tests {
    use crate::adapter::FloatPointAdapter;
    use crate::float::compatible::FloatPointCompatible;
    use crate::float::point::FloatPoint;
    use crate::float::rect::FloatRect;

    #[test]
    fn test_0() {
        let rect = FloatRect {
            min_x: 1.0,
            max_x: 1.0,
            min_y: -2.0,
            max_y: -2.0,
        };

        let adapter = FloatPointAdapter::<FloatPoint<f64>>::new(rect);

        assert_eq!(adapter.dir_scale, 1.0);
        assert_eq!(adapter.inv_scale, 1.0);
    }

    #[test]
    fn test_1() {
        let rect = FloatRect {
            min_x: 0.0,
            max_x: 10.0,
            min_y: 0.0,
            max_y: 100.0,
        };

        let adapter = FloatPointAdapter::new(rect);

        let f0 = [10.0, 2.0];
        let p0 = adapter.float_to_int(&f0);
        let f1: [f64; 2] = adapter.int_to_float(&p0);

        assert_eq!((f0.x() - f1.x()).abs() < 0.000_0001, true);
        assert_eq!((f0.y() - f1.y()).abs() < 0.000_0001, true);
    }

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

        let adapter = FloatPointAdapter::with_iter(points.iter());

        let f0 = [1.0, 2.0];
        let p0 = adapter.float_to_int(&f0);
        let f1: [f64; 2] = adapter.int_to_float(&p0);

        assert_eq!((f0.x() - f1.x()).abs() < 0.000_0001, true);
        assert_eq!((f0.y() - f1.y()).abs() < 0.000_0001, true);
    }
}
