// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod maze_struct;
mod cell;
mod generation;

use std::num::NonZeroU32;

use glam::UVec2;
use rand::SeedableRng;

use cell::Edge;
use maze_struct::Maze;

use super::Vm;

use crate::world::WorldRng;
use crate::utils::rng::Float;

impl Vm {
	pub(super) fn maze(&mut self, seed: u64, wall_thickness: NonZeroU32, open_thickness: NonZeroU32, open_prob: f32, close_prob: f32) -> Result<(), String> {
		const WALL_THICKNESS_LIMIT: u32 = 1024;
		if wall_thickness.get() > WALL_THICKNESS_LIMIT {
			return Err(format!("wall thickness too large, got {wall_thickness}, expected at most {WALL_THICKNESS_LIMIT}"));
		}

		if !(open_prob >= 0.0 && close_prob >= 0.0 && open_prob + close_prob <= 1.0) {
			return Err(format!("opening and closing probabilities must be non-negative and cannot sum to more than one, got open_prob = {open_prob} and close_prob = {close_prob}"));
		}

		let stride = wall_thickness.get() + open_thickness.get();
		let wall_count = self.grid_size.saturating_sub(UVec2::splat(open_thickness.get())) / stride;

		// If there are no walls, don't bother generating the maze
		if wall_count.cmpeq(UVec2::ZERO).any() {
			self.stack.push(vec![0.0; self.block_count].into_boxed_slice());
			return Ok(());
		}

		/*
		 * Creates the largest maze that fits within the block map.
		 *
		 * If the maze didn't fit, then it's possible that its truncation will
		 * result in some parts becoming disconnected.
		 */
		let mut maze = Maze::new(wall_count + 1);
		let maze_size = maze.get_size();
		let mut rng = WorldRng::seed_from_u64(seed);

		generation::generate(&mut maze, &mut rng);

		for y in 0..maze_size.y - 1 {
			for x in 0..maze_size.x - 1 {
				let pos = UVec2::new(x, y);
				let mut edit_edge = |edge| {
					let x = rng.next_f32();
					if x < open_prob { maze.open_cell(pos, edge); }
					else if x >= 1.0 - close_prob { maze.close_cell(pos, edge); }
				};
				edit_edge(Edge::Right);
				edit_edge(Edge::Top);
			}
		}

		let mut bmap = vec![1.0; self.block_count].into_boxed_slice();

		let mut open = |pos: UVec2| {
			if pos.cmpge(self.grid_size).any() { return; }
			bmap[(pos.x + pos.y * self.grid_size.x) as usize] = 0.0;
		};

		let mut rect = |pos: UVec2, size: UVec2| {
			for y in 0..size.y {
				for x in 0..size.x {
					open(pos + UVec2::new(x, y));
				}
			}
		};

		// Removes the edge wall of any extra space if the maze doesn't exactly fit into the grid
		let maze_max = wall_count * stride + open_thickness.get();
		rect(UVec2::new(maze_max.x, 0), UVec2::new(self.grid_size.x - maze_max.x, self.grid_size.y));
		rect(UVec2::new(0, maze_max.y), UVec2::new(self.grid_size.x, self.grid_size.y - maze_max.y));

		for y in 0..maze_size.y {
			for x in 0..maze_size.x {
				let pos = UVec2::new(x, y);
				let cell = maze.get_cell(pos).unwrap();
				assert!(cell.is_visited());

				// Removes the current cell
				let block_pos = pos * stride;
				rect(block_pos, UVec2::splat(open_thickness.get()));

				// Removes the right wall if it's open
				if cell.is_open(Edge::Right) {
					rect(block_pos + UVec2::new(open_thickness.get(), 0), UVec2::new(wall_thickness.get(), open_thickness.get()));
				}

				// Removes the top wall if it's open
				if cell.is_open(Edge::Top) {
					rect(block_pos + UVec2::new(0, open_thickness.get()), UVec2::new(open_thickness.get(), wall_thickness.get()));
				}

				// Removes the empty "points" due to a 2x2 loop
				if
					cell.is_open(Edge::Right) && cell.is_open(Edge::Top) &&
					let Some(tr_neighbour) = maze.get_cell(pos + UVec2::splat(1)) && tr_neighbour.is_open(Edge::Left) && tr_neighbour.is_open(Edge::Bottom)
				{
					rect(block_pos + UVec2::splat(open_thickness.get()), UVec2::splat(wall_thickness.get()));
				}
			}
		}

		self.stack.push(bmap);

		Ok(())
	}
}

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

	#[test]
	fn consistent_many() {
		for params in super::super::tests::random_mazes().take(5000) {
			let mut maze = Maze::new(params.size);
			let mut rng = WorldRng::seed_from_u64(params.seed);
			generation::generate(&mut maze, &mut rng);
			maze.test_consistency();
		}
	}

	#[test]
	fn consistent_rare() {
		let mut maze = Maze::new(UVec2::splat(43));
		let mut rng = WorldRng::seed_from_u64(7226261265836791453);
		generation::generate(&mut maze, &mut rng);
		maze.test_consistency();
	}

}
