/*
Copyright 2022-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of the Luminescent Dreams Tools.

Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/

/// This module contains the elements of cube coordinates.
///
/// This code is based on https://www.redblobgames.com/grids/hexagons/
use std::collections::HashSet;

/// An address within the hex coordinate system
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AxialAddr {
    q: i32,
    r: i32,
}

impl AxialAddr {
    /// Create a new axial coordinate address.
    pub fn new(q: i32, r: i32) -> Self {
        Self { q, r }
    }

    pub fn origin() -> AxialAddr {
        AxialAddr { q: 0, r: 0 }
    }

    pub fn q(&self) -> i32 {
        self.q
    }

    pub fn r(&self) -> i32 {
        self.r
    }

    pub fn s(&self) -> i32 {
        -self.q - self.r
    }

    /// Get a list of coordinates adjacent to this one.
    pub fn adjacencies(&self) -> impl Iterator<Item = AxialAddr> {
        vec![
            AxialAddr::new(self.q + 1, self.r),
            AxialAddr::new(self.q, self.r + 1),
            AxialAddr::new(self.q - 1, self.r + 1),
            AxialAddr::new(self.q - 1, self.r),
            AxialAddr::new(self.q, self.r - 1),
            AxialAddr::new(self.q + 1, self.r - 1),
        ]
        .into_iter()
    }

    /// Test whether a coordinate is adjacent to this one
    pub fn is_adjacent(&self, dest: &AxialAddr) -> bool {
        dest.adjacencies()
            .collect::<Vec<AxialAddr>>()
            .contains(self)
    }

    /// Measure the distance to a destination
    pub fn distance(&self, dest: &AxialAddr) -> usize {
        (((self.q() - dest.q()).abs() + (self.r() - dest.r()).abs() + (self.s() - dest.s()).abs())
            / 2) as usize
    }

    /// Get an iteration of all of the coordinates within the specified distance of this one.
    pub fn addresses(&self, distance: usize) -> impl Iterator<Item = AxialAddr> {
        let item = self.clone();
        let mut results: HashSet<AxialAddr> = HashSet::new();
        let mut positions: Vec<AxialAddr> = Vec::new();

        positions.push(item);

        while !positions.is_empty() {
            let elem = positions.remove(0);
            for adj in elem.adjacencies() {
                if self.distance(&adj) <= distance && !results.contains(&adj) {
                    positions.push(adj.clone());
                }
            }
            results.insert(elem);
        }

        results.into_iter()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;
    use std::collections::HashSet;

    #[test]
    fn distance_0_has_the_source() {
        let addr = AxialAddr::origin();
        let lst: Vec<AxialAddr> = addr.addresses(0).collect();
        assert_eq!(lst.len(), 1);
        assert!(lst.contains(&AxialAddr::origin()));
    }

    #[test]
    fn distance_1_has_seven_addresses() {
        let hexaddr = AxialAddr::origin();
        let lst: Vec<AxialAddr> = hexaddr.addresses(1).collect();
        assert_eq!(lst.len(), 7);
    }

    #[test]
    fn distance_2_has_19_addresses() {
        let hexaddr = AxialAddr::origin();
        let lst: Vec<AxialAddr> = hexaddr.addresses(2).collect();
        assert_eq!(lst.len(), 19);
    }

    fn address() -> (std::ops::Range<i32>, std::ops::Range<i32>) {
        (-65536_i32..65535, -65536_i32..65535)
    }

    proptest! {
        #[test]
        fn produces_adjacent_coordinates((x, y) in address(), idx in 0_usize..6) {
            let coord1 = AxialAddr::new(x, y);
            let lst1: HashSet<AxialAddr> = coord1.adjacencies().collect();
            assert_eq!(lst1.len(), 6);

            let lst1 = lst1.into_iter().collect::<Vec<_>>();
            let coord2 = &lst1[idx];
            let lst2: Vec<AxialAddr> = coord2.adjacencies().collect();
            assert!(lst2.contains(&coord1));
        }

        #[test]
        fn tests_adjacencies((q, r) in address(), idx in 0_usize..6) {
            let coord1 = AxialAddr::new(q, r);
            let lst1: Vec<AxialAddr> = coord1.adjacencies().collect();
            assert_eq!(lst1.len(), 6);

            let coord2 = &lst1[idx];
            assert!(coord2.is_adjacent(&coord1));
            assert!(coord1.is_adjacent(coord2));
        }

        #[test]
        fn measures_distance((q1, r1) in address(),
                                (q2, r2) in address()) {
            let hexaddr_1 = AxialAddr::new(q1, r1);
            let hexaddr_2 = AxialAddr::new(q2, r2);

            let s1 = -q1 - r1;
            let s2 = -q2 - r2;
            let expected = (((q1 - q2).abs() + (r1 - r2).abs() + (s1 - s2).abs()) / 2) as usize;
            assert_eq!(hexaddr_1.distance(&hexaddr_2), expected);
            assert_eq!(hexaddr_2.distance(&hexaddr_1), expected);
        }

        #[test]
        fn calculates_distance((q, r) in address(), distance in 0_usize..6) {
            let hexaddr = AxialAddr::new(q, r);
            let en_distancaj_hexaddr: Vec<AxialAddr> = hexaddr.addresses(distance).collect();

            let expected_cnt = (0..distance+1).map(|v| v * 6).fold(1, |acc, val| acc + val);
            assert_eq!(en_distancaj_hexaddr.len(), expected_cnt);
            for c in en_distancaj_hexaddr {
                assert!(c.distance(&hexaddr) <= distance);
            }
        }
    }
}