use crate::direction4::Direction4;
use crate::grid_coord_2d::{GetCoordinateBounds2D, GridCoord2D, LinearizeCoords2D};
use crate::room4::Wall4;
use crate::room4_list::{Room4List, RoomIndex};
use crate::stats::MazeStats;
use std::collections::VecDeque;
use std::ops::{Index, IndexMut};

#[derive(Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Wall4Grid {
    width: usize,
    height: usize,
    walls: Vec<Wall4>,
}

impl Wall4Grid {
    pub fn new(width: usize, height: usize) -> Self {
        Self {
            width,
            height,
            walls: vec![Wall4::ALL; width * height],
        }
    }

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

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

    pub fn remove_wall_between(&mut self, current: GridCoord2D, selected: GridCoord2D) {
        // Check adjacency
        let dx = current.x as isize - selected.x as isize;
        let dy = current.y as isize - selected.y as isize;
        assert!(
            (dx.abs() == 1 && dy == 0) || (dy.abs() == 1 && dx == 0),
            "Cells are not adjacent: {:?}, {:?}",
            current,
            selected
        );

        let cur = self.linearize_coords(current);
        let sel = self.linearize_coords(selected);

        if current.x > selected.x {
            self.walls[sel] -= Wall4::EAST;
            self.walls[cur] -= Wall4::WEST;
        } else if current.x < selected.x {
            self.walls[sel] -= Wall4::WEST;
            self.walls[cur] -= Wall4::EAST;
        }

        if current.y > selected.y {
            self.walls[sel] -= Wall4::SOUTH;
            self.walls[cur] -= Wall4::NORTH;
        } else if current.y < selected.y {
            self.walls[sel] -= Wall4::NORTH;
            self.walls[cur] -= Wall4::SOUTH;
        }
    }

    pub fn width(&self) -> usize {
        self.width
    }

    pub fn height(&self) -> usize {
        self.height
    }

    pub fn coords(&self) -> impl Iterator<Item = GridCoord2D> + '_ {
        (0..self.height).flat_map(move |y| (0..self.width).map(move |x| GridCoord2D::new(x, y)))
    }

    pub fn neighbors(&self, cell: GridCoord2D) -> impl Iterator<Item = GridCoord2D> + '_ {
        let mut out = [None, None, None, None];
        let mut n = 0;
        if let Some(c) = cell.up().filter(|c| c.y < self.height) {
            out[n] = Some(c);
            n += 1;
        }
        if let Some(c) = cell.right().filter(|c| c.x < self.width) {
            out[n] = Some(c);
            n += 1;
        }
        if let Some(c) = cell.down().filter(|c| c.y < self.height) {
            out[n] = Some(c);
            n += 1;
        }
        if let Some(c) = cell.left().filter(|c| c.x < self.width) {
            out[n] = Some(c);
        }
        out.into_iter().flatten()
    }

    pub fn open_neighbors(&self, cell: GridCoord2D) -> impl Iterator<Item = GridCoord2D> + '_ {
        let mut out = [None, None, None, None];
        let mut n = 0;
        if let Some(walls) = self.get(cell) {
            if !walls.contains(Direction4::NORTH) {
                if let Some(c) = cell.up().filter(|c| c.y < self.height) {
                    out[n] = Some(c);
                    n += 1;
                }
            }
            if !walls.contains(Direction4::EAST) {
                if let Some(c) = cell.right().filter(|c| c.x < self.width) {
                    out[n] = Some(c);
                    n += 1;
                }
            }
            if !walls.contains(Direction4::SOUTH) {
                if let Some(c) = cell.down().filter(|c| c.y < self.height) {
                    out[n] = Some(c);
                    n += 1;
                }
            }
            if !walls.contains(Direction4::WEST) {
                if let Some(c) = cell.left().filter(|c| c.x < self.width) {
                    out[n] = Some(c);
                }
            }
        }
        out.into_iter().flatten()
    }

    pub fn to_room_list<Tag, F>(&self, tag_fn: F) -> Room4List<Tag>
    where
        F: Fn(GridCoord2D) -> Tag,
    {
        let mut list = Room4List::default();
        let mut index_of_cell =
            vec![RoomIndex::from(0).expect("0 is valid"); self.width * self.height];

        for cell in self.coords() {
            let index = list.push_default(tag_fn(cell));
            index_of_cell[self.linearize_coords(cell)] = index;
        }

        for cell in self.coords() {
            let idx = index_of_cell[self.linearize_coords(cell)];
            let doors = !self[cell];
            let room = list.get_mut(idx).expect("new room index must exist");
            if doors.contains(Direction4::NORTH) {
                if let Some(n) = cell.up().filter(|c| c.y < self.height) {
                    room.set_north(index_of_cell[self.linearize_coords(n)]);
                }
            }
            if doors.contains(Direction4::EAST) {
                if let Some(e) = cell.right().filter(|c| c.x < self.width) {
                    room.set_east(index_of_cell[self.linearize_coords(e)]);
                }
            }
            if doors.contains(Direction4::SOUTH) {
                if let Some(s) = cell.down().filter(|c| c.y < self.height) {
                    room.set_south(index_of_cell[self.linearize_coords(s)]);
                }
            }
            if doors.contains(Direction4::WEST) {
                if let Some(w) = cell.left().filter(|c| c.x < self.width) {
                    room.set_west(index_of_cell[self.linearize_coords(w)]);
                }
            }
        }

        list
    }

    pub fn stats(&self) -> MazeStats {
        MazeStats::from_grid(self)
    }

    pub(crate) fn bfs_distances(&self, start: GridCoord2D) -> Vec<Option<usize>> {
        let mut dist = vec![None; self.width * self.height];
        if self.get(start).is_none() {
            return dist;
        }

        let mut q = VecDeque::new();
        dist[self.linearize_coords(start)] = Some(0);
        q.push_back(start);

        while let Some(cell) = q.pop_front() {
            let base = dist[self.linearize_coords(cell)].unwrap_or(0);
            for n in self.open_neighbors(cell) {
                let idx = self.linearize_coords(n);
                if dist[idx].is_none() {
                    dist[idx] = Some(base + 1);
                    q.push_back(n);
                }
            }
        }

        dist
    }
}

impl From<&Wall4Grid> for Room4List<()> {
    fn from(value: &Wall4Grid) -> Self {
        value.to_room_list(|_| ())
    }
}

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

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

impl Index<GridCoord2D> for Wall4Grid {
    type Output = Wall4;

    fn index(&self, index: GridCoord2D) -> &Self::Output {
        let index = self.linearize_coords(index);
        &self.walls[index]
    }
}

impl IndexMut<GridCoord2D> for Wall4Grid {
    fn index_mut(&mut self, index: GridCoord2D) -> &mut Self::Output {
        let index = self.linearize_coords(index);
        &mut self.walls[index]
    }
}
