// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use arrayvec::ArrayVec;
use glam::UVec2;
use strum::{EnumCount, IntoEnumIterator};
use rand::Rng;

use super::cell::{Cell, Edge};

pub(super) struct Maze {
	size: UVec2,
	grid: Box<[Cell]>,
}

impl Maze {
	/**
	 * Creates a new maze with all cells initialised to being unvisited and
	 * having all four walls.
	 */
	pub fn new(size: UVec2) -> Maze {
		Maze {
			size,
			grid: vec![Cell::new(); size.element_product() as usize].into_boxed_slice(),
		}
	}

	/** Gets the size of the maze. */
	pub fn get_size(&self) -> UVec2 { self.size }

	/**
	 * Opens the given edge at the cell at position `pos` and the opposite edge
	 * of that cell's neighbour.
	 *
	 * Panics if `pos` or `pos + edge.to_dir()` are out of bounds.
	 */
	pub fn open_cell(&mut self, pos: UVec2, edge: Edge) {
		self.get_cell_mut(pos).unwrap().open(edge);
		self.get_cell_mut(pos + edge).unwrap().open(edge.opposite());
	}

	/**
	 * Closes the given edge at the cell at position `pos` and the opposite edge
	 * of that cell's neighbour.
	 *
	 * Panics if `pos` or `pos + edge.to_dir()` are out of bounds.
	 */
	pub fn close_cell(&mut self, pos: UVec2, edge: Edge) {
		self.get_cell_mut(pos).unwrap().close(edge);
		self.get_cell_mut(pos + edge).unwrap().close(edge.opposite());
	}

	/**
	 * Marks the cell at position `pos` as visited.
	 *
	 * Panics if `pos` is out of bounds.
	 */
	pub fn visit_cell(&mut self, pos: UVec2) {
		self.get_cell_mut(pos).unwrap().visit();
	}

	/**
	 * Returns the cell at position `pos`.
	 *
	 * Panics if `pos` is out of bounds.
	 */
	pub fn get_cell(&self, pos: UVec2) -> Option<Cell> {
		self.in_bounds(pos).then(|| self.grid[(pos.x + pos.y * self.size.x) as usize])
	}

	/**
	 * Returns a mutable reference to the cell at position `pos` if not out of
	 * bounds. Otherwise returns None.
	 */
	fn get_cell_mut(&mut self, pos: UVec2) -> Option<&mut Cell> {
		self.in_bounds(pos).then(|| &mut self.grid[(pos.x + pos.y * self.size.x) as usize])
	}

	/** Returns whether the given position is in bounds. */
	fn in_bounds(&self, pos: UVec2) -> bool {
		pos.cmplt(self.size).all()
	}

	pub fn random_unvisited_neighbour(&mut self, pos: UVec2, rng: &mut impl Rng) -> Option<Edge> {
		let mut neighbours: ArrayVec<Edge, { Edge::COUNT }> = ArrayVec::new();
		for edge in Edge::iter() {
			if self.get_cell_mut(pos + edge).is_some_and(|cell| !cell.is_visited()) {
				neighbours.push(edge);
			}
		}

		(!neighbours.is_empty()).then(|| neighbours[rng.next_u32() as usize % neighbours.len()])
	}

	#[cfg(test)]
	pub fn test_consistency(&self) {
		for y in 0..self.size.y {
			for x in 0..self.size.x {
				let pos = UVec2::new(x, y);
				let cell = self.get_cell(pos).unwrap();
				assert!(cell.is_visited()); // Should be all visited
				if let Some(cell_left) = self.get_cell(pos + Edge::Left) { assert_eq!(cell.is_open(Edge::Left), cell_left.is_open(Edge::Right)); }
				if let Some(cell_right) = self.get_cell(pos + Edge::Right) { assert_eq!(cell.is_open(Edge::Right), cell_right.is_open(Edge::Left)); }
				if let Some(cell_down) = self.get_cell(pos + Edge::Bottom) { assert_eq!(cell.is_open(Edge::Bottom), cell_down.is_open(Edge::Top)); }
				if let Some(cell_up) = self.get_cell(pos + Edge::Top) { assert_eq!(cell.is_open(Edge::Top), cell_up.is_open(Edge::Bottom)); }
				let count = cell.is_open(Edge::Left) as u32 + cell.is_open(Edge::Right) as u32 + cell.is_open(Edge::Bottom) as u32 + cell.is_open(Edge::Top) as u32;

				// 1x1 mazes are always closed
				assert!(count > 0 || self.size == UVec2::ONE);
			}
		}
	}
}
