use std::ops::Range;

use super::{AsRangedCoord, DiscreteRanged, Ranged, ReversibleRanged};

macro_rules! impl_discrete_trait {
    ($name:ident) => {
        impl DiscreteRanged for $name {
            type RangeParameter = ();
            fn get_range_parameter(&self) -> () {}
            fn next_value(this: &Self::ValueType, _: &()) -> Self::ValueType {
                return *this + 1;
            }
            fn previous_value(this: &Self::ValueType, _: &()) -> Self::ValueType {
                return *this - 1;
            }
        }
    };
}

macro_rules! impl_ranged_type_trait {
    ($value:ty, $coord:ident) => {
        impl AsRangedCoord for Range<$value> {
            type CoordDescType = $coord;
            type Value = $value;
        }
    };
}

macro_rules! make_numeric_coord {
    ($type:ty, $name:ident, $key_points:ident, $doc: expr) => {
        #[doc = $doc]
        #[derive(Clone)]
        pub struct $name($type, $type);
        impl From<Range<$type>> for $name {
            fn from(range: Range<$type>) -> Self {
                return $name(range.start, range.end);
            }
        }
        impl Ranged for $name {
            type ValueType = $type;
            fn map(&self, v: &$type, limit: (i32, i32)) -> i32 {
                let logic_length = (*v - self.0) as f64 / (self.1 - self.0) as f64;
                let actual_length = limit.1 - limit.0;

                if actual_length == 0 {
                    return limit.1;
                }

                return limit.0 + (actual_length as f64 * logic_length + 1e-3).floor() as i32;
            }
            fn key_points(&self, max_points: usize) -> Vec<$type> {
                $key_points((self.0, self.1), max_points)
            }
            fn range(&self) -> Range<$type> {
                return self.0..self.1;
            }
        }

        impl ReversibleRanged for $name {
            fn unmap(&self, p:i32, (min,max): (i32, i32)) -> Option<$type> {
                if p < min.min(max) || p > max.max(min) {
                    return None;
                }

                let logical_offset = (p - min) as f64 / (max - min) as f64;

                return Some(((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type);
            }
        }
    };
}

macro_rules! gen_key_points_comp {
    (float, $name:ident, $type:ty) => {
        fn $name(range: ($type, $type), max_points: usize) -> Vec<$type> {
            if max_points == 0 {
                return vec![];
            }

            let range = (range.0 as f64, range.1 as f64);
            let mut scale = (10f64).powf((range.1 - range.0).log(10.0).floor());
            let mut digits = -(range.1 - range.0).log(10.0).floor() as i32 + 1;
            fn rem_euclid(a: f64, b: f64) -> f64 {
                if b > 0.0 {
                    a - (a / b).floor() * b
                } else {
                    a - (a / b).ceil() * b
                }
            }

            // At this point we need to make sure that the loop invariant:
            // The scale must yield number of points than requested
            if 1 + ((range.1 - range.0) / scale).floor() as usize > max_points {
                scale *= 10.0;
            }

            'outer: loop {
                let old_scale = scale;
                for nxt in [2.0, 5.0, 10.0].iter() {
                    let new_left = range.0 + scale / nxt - rem_euclid(range.0, scale / nxt);
                    let new_right = range.1 - rem_euclid(range.1, scale / nxt);

                    let npoints = 1 + ((new_right - new_left) / old_scale * nxt) as usize;

                    if npoints > max_points {
                        break 'outer;
                    }

                    scale = old_scale / nxt;
                }
                scale = old_scale / 10.0;
                if scale < 1.0 {
                    digits += 1;
                }
            }

            let mut ret = vec![];
            let mut left = range.0 + scale - rem_euclid(range.0, scale);
            let right = range.1 - rem_euclid(range.1, scale);
            while left <= right {
                let size = (10f64).powf(digits as f64 + 1.0);
                let new_left = (left * size).abs() + 1e-3;
                if left < 0.0 {
                    left = -new_left.round() / size;
                } else {
                    left = new_left.round() / size;
                }
                ret.push(left as $type);
                left += scale;
            }
            return ret;
        }
    };
    (integer, $name:ident, $type:ty) => {
        fn $name(range: ($type, $type), max_points: usize) -> Vec<$type> {
            let mut scale: $type = 1;
            let range = (range.0.min(range.1), range.0.max(range.1));
            'outer: while (range.1 - range.0 + scale - 1) as usize / (scale as usize) > max_points {
                let next_scale = scale * 10;
                for new_scale in [scale * 2, scale * 5, scale * 10].iter() {
                    scale = *new_scale;
                    if (range.1 - range.0 + *new_scale - 1) as usize / (*new_scale as usize)
                        < max_points
                    {
                        break 'outer;
                    }
                }
                scale = next_scale;
            }

            let (mut left, right) = (
                range.0 + (scale - range.0 % scale) % scale,
                range.1 - range.1 % scale,
            );

            let mut ret = vec![];
            while left <= right {
                ret.push(left as $type);
                left += scale;
            }

            return ret;
        }
    };
}

gen_key_points_comp!(float, compute_f32_key_points, f32);
gen_key_points_comp!(float, compute_f64_key_points, f64);
gen_key_points_comp!(integer, compute_i32_key_points, i32);
gen_key_points_comp!(integer, compute_u32_key_points, u32);
gen_key_points_comp!(integer, compute_i64_key_points, i64);
gen_key_points_comp!(integer, compute_u64_key_points, u64);
gen_key_points_comp!(integer, compute_i128_key_points, i128);
gen_key_points_comp!(integer, compute_u128_key_points, u128);
gen_key_points_comp!(integer, compute_isize_key_points, isize);
gen_key_points_comp!(integer, compute_usize_key_points, usize);

make_numeric_coord!(
    f32,
    RangedCoordf32,
    compute_f32_key_points,
    "The ranged coordinate for type f32"
);
make_numeric_coord!(
    f64,
    RangedCoordf64,
    compute_f64_key_points,
    "The ranged coordinate for type f64"
);
make_numeric_coord!(
    u32,
    RangedCoordu32,
    compute_u32_key_points,
    "The ranged coordinate for type u32"
);
make_numeric_coord!(
    i32,
    RangedCoordi32,
    compute_i32_key_points,
    "The ranged coordinate for type i32"
);
make_numeric_coord!(
    u64,
    RangedCoordu64,
    compute_u64_key_points,
    "The ranged coordinate for type u64"
);
make_numeric_coord!(
    i64,
    RangedCoordi64,
    compute_i64_key_points,
    "The ranged coordinate for type i64"
);
make_numeric_coord!(
    u128,
    RangedCoordu128,
    compute_u128_key_points,
    "The ranged coordinate for type u128"
);
make_numeric_coord!(
    i128,
    RangedCoordi128,
    compute_i128_key_points,
    "The ranged coordinate for type i128"
);
make_numeric_coord!(
    usize,
    RangedCoordusize,
    compute_usize_key_points,
    "The ranged coordinate for type usize"
);
make_numeric_coord!(
    isize,
    RangedCoordisize,
    compute_isize_key_points,
    "The ranged coordinate for type isize"
);

impl_discrete_trait!(RangedCoordu32);
impl_discrete_trait!(RangedCoordi32);
impl_discrete_trait!(RangedCoordu64);
impl_discrete_trait!(RangedCoordi64);
impl_discrete_trait!(RangedCoordu128);
impl_discrete_trait!(RangedCoordi128);
impl_discrete_trait!(RangedCoordusize);
impl_discrete_trait!(RangedCoordisize);

impl_ranged_type_trait!(f32, RangedCoordf32);
impl_ranged_type_trait!(f64, RangedCoordf64);
impl_ranged_type_trait!(i32, RangedCoordi32);
impl_ranged_type_trait!(u32, RangedCoordu32);
impl_ranged_type_trait!(i64, RangedCoordi64);
impl_ranged_type_trait!(u64, RangedCoordu64);
impl_ranged_type_trait!(i128, RangedCoordi128);
impl_ranged_type_trait!(u128, RangedCoordu128);
impl_ranged_type_trait!(isize, RangedCoordisize);
impl_ranged_type_trait!(usize, RangedCoordusize);

// TODO: Think about how to re-organize this part
pub mod group_integer_by {
    use super::Ranged;
    use super::{AsRangedCoord, DiscreteRanged};
    use num_traits::{FromPrimitive, PrimInt, ToPrimitive};
    use std::ops::{Mul, Range};

    /// The ranged value spec that needs to be grouped.
    /// This is useful, for example, when we have an X axis is a integer and denotes days.
    /// And we are expecting the tick mark denotes weeks, in this way we can make the range
    /// spec grouping by 7 elements.
    pub struct GroupBy<T>(T, T::ValueType)
    where
        T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
        T: Ranged;

    /// The trait that provides method `Self::group_by` function which creates a
    /// `GroupBy` decorated ranged value.
    pub trait ToGroupByRange
    where
        Self: AsRangedCoord,
        <Self as AsRangedCoord>::Value: PrimInt + ToPrimitive + FromPrimitive + Mul,
        <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType:
            PrimInt + ToPrimitive + FromPrimitive + Mul,
    {
        /// Make a grouping ranged value, see the documentation for `GroupBy` for details.
        ///
        /// - `value`: The number of values we want to group it
        /// - **return**: The newly created grouping range sepcification
        fn group_by(
            self,
            value: <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType,
        ) -> GroupBy<<Self as AsRangedCoord>::CoordDescType> {
            GroupBy(self.into(), value)
        }
    }

    impl<T> ToGroupByRange for T
    where
        Self: AsRangedCoord,
        <Self as AsRangedCoord>::Value: PrimInt + FromPrimitive + ToPrimitive + Mul,
        <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType:
            PrimInt + FromPrimitive + ToPrimitive + Mul,
    {
    }

    impl<T> AsRangedCoord for GroupBy<T>
    where
        T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
        T: Ranged,
    {
        type Value = T::ValueType;
        type CoordDescType = Self;
    }

    impl<T> DiscreteRanged for GroupBy<T>
    where
        T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
        T: Ranged + DiscreteRanged,
    {
        type RangeParameter = <T as DiscreteRanged>::RangeParameter;
        fn get_range_parameter(&self) -> Self::RangeParameter {
            self.0.get_range_parameter()
        }
        fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
            <T as DiscreteRanged>::previous_value(this, param)
        }
        fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
            <T as DiscreteRanged>::next_value(this, param)
        }
    }

    impl<T> Ranged for GroupBy<T>
    where
        T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
        T: Ranged,
    {
        type ValueType = T::ValueType;
        fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 {
            self.0.map(value, limit)
        }
        fn range(&self) -> Range<T::ValueType> {
            self.0.range()
        }
        fn key_points(&self, max_points: usize) -> Vec<T::ValueType> {
            let actual_range = self.0.range();
            let from = ((actual_range.start + self.1 - T::ValueType::from_u8(1).unwrap()) / self.1)
                .to_isize()
                .unwrap();
            let to = (actual_range.end / self.1).to_isize().unwrap();
            let logic_range: super::RangedCoordisize = (from..to).into();

            logic_range
                .key_points(max_points)
                .into_iter()
                .map(|x| T::ValueType::from_isize(x).unwrap() * self.1)
                .collect()
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::coord::*;
    #[test]
    fn test_key_points() {
        let kp = compute_i32_key_points((0, 999), 28);

        assert!(kp.len() > 0);
        assert!(kp.len() <= 28);

        let kp = compute_f64_key_points((-1.2, 1.2), 1);
        assert!(kp.len() == 1);

        let kp = compute_f64_key_points((-1.2, 1.2), 0);
        assert!(kp.len() == 0);
    }

    #[test]
    fn test_linear_coord_map() {
        let coord: RangedCoordu32 = (0..20).into();
        assert_eq!(coord.key_points(11).len(), 11);
        assert_eq!(coord.key_points(11)[0], 0);
        assert_eq!(coord.key_points(11)[10], 20);
        assert_eq!(coord.map(&5, (0, 100)), 25);

        let coord: RangedCoordf32 = (0f32..20f32).into();
        assert_eq!(coord.map(&5.0, (0, 100)), 25);
    }

    #[test]
    fn test_linear_coord_system() {
        let _coord =
            RangedCoord::<RangedCoordu32, RangedCoordu32>::new(0..10, 0..10, (0..1024, 0..768));
    }
}
