//! Hex directions

use std::iter::{ExactSizeIterator, FusedIterator, Iterator};
use std::ops::Neg;

/// All hex directions in sequential order.
///
/// - Starting from North-east in clockwise order in in pointy grids where +X =
///   East and +Y = North-east.
/// - Starting from North in clockwise order in in flat grids where +X =
///   North-east and +Y = North.
#[repr(u8)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "serde-1", derive(Serialize, Deserialize))]
pub enum Direction {
    YZ = 0,
    XZ = 1,
    XY = 2,
    ZY = 3,
    ZX = 4,
    YX = 5,
    UP = 6,
    DOWN = 7,
}

impl Direction {
    /// Returns the direction rotated "clockwise" around the W axis. `UP` and
    /// `DOWN` are returned unchanged.
    pub fn clockwise(self) -> Direction {
        use Direction as D;
        match self {
            D::XZ => D::ZY,
            D::XY => D::ZX,
            D::ZY => D::YX,
            D::ZX => D::YZ,
            D::YX => D::XZ,
            D::YZ => D::XY,
            D::UP => D::UP,
            D::DOWN => D::DOWN,
        }
    }

    /// Returns the direction rotated "counter-clockwise" around the W axis.
    /// `UP` and  `DOWN` are returned unchanged.
    pub fn counter_clockwise(self) -> Direction {
        use Direction as D;
        match self {
            D::XZ => D::YX,
            D::XY => D::YZ,
            D::ZY => D::XZ,
            D::ZX => D::XY,
            D::YX => D::ZY,
            D::YZ => D::ZX,
            D::UP => D::UP,
            D::DOWN => D::DOWN,
        }
    }

    /// Returns an iterator through all `Direction`s, including `UP` and `DOWN`.
    pub fn iter() -> Directions {
        Directions(0)
    }

    /// Returns an iterator through planar `Direction`s, excluding `UP` and `DOWN`.
    pub fn iter_planar() -> PlanarDirections {
        PlanarDirections(0)
    }
}

impl Neg for Direction {
    type Output = Self;

    fn neg(self) -> Direction {
        use Direction as D;
        match self {
            D::YZ => D::ZY,
            D::XZ => D::ZX,
            D::XY => D::YX,
            D::ZY => D::YZ,
            D::ZX => D::XZ,
            D::YX => D::XY,
            D::UP => D::DOWN,
            D::DOWN => D::UP,
        }
    }
}

/// Error indicating that the input number is not a Direction
#[derive(Debug)]
pub struct NotDirection;

impl std::convert::TryFrom<u8> for Direction {
    type Error = NotDirection;

    fn try_from(num: u8) -> Result<Self, NotDirection> {
        if num < 8 {
            // Safe to transmute because num < 8, and Direction is repr(u8)
            Ok(unsafe { std::mem::transmute(num as u8) })
        } else {
            Err(NotDirection)
        }
    }
}

/// An iterator through all 8 directions, including `UP` and `DOWN`.
#[derive(Debug)]
pub struct Directions(usize);

impl Iterator for Directions {
    type Item = Direction;

    fn next(&mut self) -> Option<Direction> {
        if self.0 < 8 {
            // Safe to transmute because self.0 < 8, and Direction is repr(u8)
            let result = unsafe { std::mem::transmute(self.0 as u8) };
            self.0 += 1;
            Some(result)
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (8 - self.0, Some(8 - self.0))
    }
}
impl FusedIterator for Directions {}
impl ExactSizeIterator for Directions {}

/// An iterator through the 6 planar directions, not including `UP` and `DOWN`.
#[derive(Debug)]
pub struct PlanarDirections(usize);

impl Iterator for PlanarDirections {
    type Item = Direction;

    fn next(&mut self) -> Option<Direction> {
        if self.0 < 6 {
            // Safe to transmute because self.0 < 8, and Direction is repr(u8)
            let result = unsafe { std::mem::transmute(self.0 as u8) };
            self.0 += 1;
            Some(result)
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (8 - self.0, Some(6 - self.0))
    }
}
impl FusedIterator for PlanarDirections {}
impl ExactSizeIterator for PlanarDirections {}

#[cfg(feature = "rand-07")]
mod distribution_impl {
    use super::Direction;

    use std::convert::TryFrom;

    use rand::distributions::{Distribution, Standard};
    use rand::Rng;

    /// A distribution that samples only from the planar directions.
    #[derive(Copy, Clone, Debug, Default)]
    pub struct Planar;

    impl Distribution<Direction> for Planar {
        fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Direction {
            Direction::try_from(rng.gen_range(0, 6))
                .expect("rng should return number in range 0..6")
        }
    }

    impl Distribution<Direction> for Standard {
        fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Direction {
            Direction::try_from(rng.gen_range(0, 8))
                .expect("rng should return number in range 0..8")
        }
    }
}

#[cfg(feature = "rand-07")]
#[doc(inline)]
pub use distribution_impl::Planar;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn can_iter() {
        use Direction as D;
        assert_eq!(
            vec![D::YZ, D::XZ, D::XY, D::ZY, D::ZX, D::YX, D::UP, D::DOWN,],
            Direction::iter().collect::<Vec<_>>(),
        );
    }

    #[test]
    fn can_iter_planar() {
        use Direction as D;
        assert_eq!(
            vec![D::YZ, D::XZ, D::XY, D::ZY, D::ZX, D::YX,],
            Direction::iter_planar().collect::<Vec<_>>(),
        );
    }
}
