//! Discrete line iterator in hex space.

use num_traits::AsPrimitive;
use std::iter::{FusedIterator, Iterator};
use std::marker::PhantomData;

use super::coords::{Point, Vector};

type HexPointF = Point<f32>;
type HexVectorF = Vector<f32>;

/// A discrete line iterator in hex space.
pub struct Line<T> {
    current: HexPointF,
    step_size: HexVectorF,
    steps: usize,
    _phantom: PhantomData<T>,
}

impl<T> Line<T>
where
    T: AsPrimitive<f32>,
    f32: AsPrimitive<T>,
{
    /// Create a new line iterator from endpoints.
    ///
    /// Generally, it's easier to use `Point::line_to` variants instead.
    pub fn new(from: Point<T>, to: Point<T>, exclude_start: bool, exclude_end: bool) -> Self {
        let mut current = from.cast_fix();
        let delta = to.cast_fix() - current;

        // length is known to be non-negative
        #[allow(clippy::cast_sign_loss)]
        let mut steps = delta.length() as usize + 1;

        let step_size = delta * (1.0 / ((steps - 1) as f32));

        if exclude_start && steps > 0 {
            current += step_size;
            steps -= 1;
        }

        if exclude_end && steps > 0 {
            steps -= 1;
        }

        Line {
            current,
            step_size,
            steps,
            _phantom: PhantomData,
        }
    }
}

impl<T> Iterator for Line<T>
where
    T: 'static + Copy,
    f32: AsPrimitive<T>,
{
    type Item = Point<T>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.steps > 0 {
            let result = self.current;
            self.current += self.step_size;
            self.steps -= 1;
            Some(result.cast_round())
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.steps, Some(self.steps))
    }
}

impl<T> ExactSizeIterator for Line<T>
where
    T: 'static + Copy,
    f32: AsPrimitive<T>,
{
    fn len(&self) -> usize {
        self.steps
    }
}

impl<T> FusedIterator for Line<T>
where
    T: 'static + Copy,
    f32: AsPrimitive<T>,
{
}

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

    type H = super::super::coords::Point<i16>;
    type V = super::super::coords::Vector<i16>;

    #[test]
    fn can_iter() {
        fn test_endpoints(from: H, to: H, expected_steps: &[V]) {
            let expected = expected_steps
                .iter()
                .scan(from, |s, &d| {
                    *s += d;
                    Some(*s)
                })
                .collect::<Vec<_>>();

            itertools::assert_equal(Line::new(from, to, false, false), expected.iter().cloned());
            itertools::assert_equal(
                Line::new(from, to, true, false),
                expected[1..].iter().cloned(),
            );
            itertools::assert_equal(
                Line::new(from, to, false, true),
                expected[..expected.len() - 1].iter().cloned(),
            );
            itertools::assert_equal(
                Line::new(from, to, true, true),
                expected[1..expected.len() - 1].iter().cloned(),
            );

            itertools::assert_equal(
                Line::new(to, from, false, false),
                expected.iter().cloned().rev(),
            );
            itertools::assert_equal(
                Line::new(to, from, true, false),
                expected[..expected.len() - 1].iter().cloned().rev(),
            );
            itertools::assert_equal(
                Line::new(to, from, false, true),
                expected[1..].iter().cloned().rev(),
            );
            itertools::assert_equal(
                Line::new(to, from, true, true),
                expected[1..expected.len() - 1].iter().cloned().rev(),
            );
        }

        test_endpoints(
            H::new(-1, 1, 0, 0),
            H::new(0, -1, 1, 0),
            &[V::zero(), V::zy(), V::xy()],
        );
        test_endpoints(
            H::new(-1, 1, 0, 0),
            H::new(1, -2, 1, 0),
            &[V::zero(), V::xy(), V::zy(), V::xy()],
        );
        test_endpoints(
            H::new(-1, 1, 0, 0),
            H::new(0, 2, -2, 0),
            &[V::zero(), V::yz(), V::xz()],
        );
    }
}
