use crate::grid_coord_2d::{GetCoordinateBounds2D, GridCoord2D, LinearizeCoords2D};
use crate::wall4_grid::Wall4Grid;
use std::ops::Index;

#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AdjacencyList {
    pub width: usize,
    pub height: usize,
    pub neighbors: Vec<Vec<GridCoord2D>>,
}

impl AdjacencyList {
    pub fn neighbors(&self, coord: GridCoord2D) -> &[GridCoord2D] {
        &self.neighbors[self.linearize_coords(coord)]
    }

    pub fn get_neighbors(&self, coord: GridCoord2D) -> Option<&[GridCoord2D]> {
        if coord.x >= self.width || coord.y >= self.height {
            return None;
        }
        Some(self.neighbors(coord))
    }
}

impl GetCoordinateBounds2D for AdjacencyList {
    #[inline]
    fn width(&self) -> usize {
        self.width
    }

    #[inline]
    fn height(&self) -> usize {
        self.height
    }
}

impl Index<GridCoord2D> for AdjacencyList {
    type Output = [GridCoord2D];

    fn index(&self, index: GridCoord2D) -> &Self::Output {
        self.neighbors(index)
    }
}

impl From<&Wall4Grid> for AdjacencyList {
    fn from(value: &Wall4Grid) -> Self {
        let mut neighbors = vec![Vec::with_capacity(4); value.width() * value.height()];

        for cell in value.coords() {
            neighbors[value.linearize_coords(cell)] = value.open_neighbors(cell).collect();
        }

        Self {
            width: value.width(),
            height: value.height(),
            neighbors,
        }
    }
}

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

    #[test]
    fn adjacency_matches_grid_open_neighbors() {
        let maze = RecursiveBacktracker4::new_from_seed(7).generate(8, 8);
        let adjacency = AdjacencyList::from(&maze);

        for coord in maze.coords() {
            assert_eq!(
                adjacency.neighbors(coord),
                maze.open_neighbors(coord).collect::<Vec<_>>()
            );
        }
    }

    #[test]
    fn get_neighbors_returns_none_out_of_bounds() {
        let maze = RecursiveBacktracker4::new_from_seed(7).generate(8, 8);
        let adjacency = AdjacencyList::from(&maze);

        assert!(
            adjacency
                .get_neighbors(GridCoord2D::new(adjacency.width, 0))
                .is_none()
        );
        assert!(
            adjacency
                .get_neighbors(GridCoord2D::new(0, adjacency.height))
                .is_none()
        );
    }

    #[test]
    fn index_trait_matches_neighbors_method() {
        let maze = RecursiveBacktracker4::new_from_seed(7).generate(8, 8);
        let adjacency = AdjacencyList::from(&maze);

        for coord in maze.coords() {
            assert_eq!(&adjacency[coord], adjacency.neighbors(coord));
        }
    }

    #[test]
    fn neighbor_symmetry() {
        let maze = RecursiveBacktracker4::new_from_seed(7).generate(8, 8);
        let adjacency = AdjacencyList::from(&maze);

        for coord in maze.coords() {
            for neighbor in adjacency.neighbors(coord) {
                assert!(adjacency.neighbors(*neighbor).contains(&coord));
            }
        }
    }

    #[test]
    fn empty_grid_produces_empty_adjacency() {
        let maze = RecursiveBacktracker4::new_from_seed(7).generate(0, 0);
        let adjacency = AdjacencyList::from(&maze);

        assert_eq!(adjacency.width, 0);
        assert_eq!(adjacency.height, 0);
        assert!(adjacency.neighbors.is_empty());
    }
}
