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

pub use range_iter::RangeIterator;

use std::num::NonZeroU32;

use glam::{UVec2, Vec2};
#[cfg(feature = "client")] use glam::IVec2;
use serde::{Serialize, Deserialize};

#[derive(Clone, Serialize, Deserialize)]
pub struct Blocks {
	block_size: NonZeroU32,
	grid_size: UVec2,
	data: Box<[bool]>,
}

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
#[cfg(feature = "client")]
pub struct BlockBorder(IVec2, IVec2);

pub const MAX_BLOCKS: usize = 1048576;

impl Blocks {
	pub fn new(block_size: NonZeroU32, grid_size: UVec2, data: Box<[bool]>) -> Blocks {
		Blocks { block_size, grid_size, data }
	}

	#[cfg(feature = "client")]
	pub fn validate(&self) -> Result<(), &'static str> {
		if self.data.len() <= MAX_BLOCKS { Ok(()) }
		else { Err("too many blocks") }
	}

	#[cfg(feature = "client")]
	pub fn get(&self, block_pos: IVec2) -> Option<bool> {
		self.block_pos_in_bounds(block_pos).then(|| self.data[(block_pos.x + block_pos.y * self.grid_size.x as i32) as usize])
	}

	#[cfg(feature = "client")]
	pub fn get_grid_size(&self) -> UVec2 {
		self.grid_size
	}

	#[cfg(feature = "client")]
	pub fn get_block_size(&self) -> NonZeroU32 {
		self.block_size
	}

	#[cfg(feature = "client")]
	pub fn get_data(&self) -> &[bool] {
		&self.data
	}

	#[cfg(feature = "client")]
	pub fn world_to_block_pos(&self, world_pos: Vec2) -> IVec2 {
		let block_size = self.block_size.get() as f32;
		((world_pos + self.get_offset(block_size)) / block_size).floor().as_ivec2()
	}

	/**
	 * Returns the centre of the input grid cell.
	 */
	#[cfg(feature = "client")]
	pub fn block_to_world_pos(&self, block_pos: IVec2) -> Vec2 {
		let block_size = self.block_size.get() as f32;
		(block_pos.as_vec2() + Vec2::splat(0.5)) * block_size - self.get_offset(block_size)
	}

	/**
	 * Takes a `world_size` ≥ 0 as input and returns the world's border in block coordinates.
	 *
	 * The block border returned will have the following two properties (unless
	 * things are very large that floating-point imprecision gets involved, but I
	 * don't care about those cases):
	 * 1. All cells that are *completely* inside the world are considered inside
	 * 	the world.
	 * 2. All cells that have *any part* that are outside the world are
	 * 	considered outside the world.
	 */
	#[cfg(feature = "client")]
	pub fn world_size_to_block_border(&self, world_size: Vec2) -> BlockBorder {
		if !world_size.cmpge(Vec2::splat(0.0)).all() {
			log::warn!("invalid world size, {world_size}");
			return BlockBorder(IVec2::MAX, IVec2::MIN);
		}

		let world_radii = world_size / 2.0;
		let block_size = self.block_size.get() as f32;
		let offset = self.get_offset(block_size);
		BlockBorder(((-world_radii + offset) / block_size).ceil().as_ivec2(), (((world_radii + offset) / block_size).floor() - Vec2::ONE).as_ivec2())
	}

	#[cfg(feature = "client")]
	pub fn block_pos_in_bounds(&self, block_pos: IVec2) -> bool {
		block_pos.x >= 0 && (block_pos.x as u32) < self.grid_size.x && block_pos.y >= 0 && (block_pos.y as u32) < self.grid_size.y
	}

	#[cfg(feature = "client")]
	pub fn get_offset(&self, block_size: f32) -> Vec2 {
		block_size * self.grid_size.as_vec2() / 2.0
	}

	/**
	 * Returns an iterator over all blocks colliding with the square centred at
	 * `pos` with radius (half side length) of `radius`.
	 */
	pub fn iter_range(&self, pos: Vec2, radius: f32) -> RangeIterator<'_> {
		debug_assert!(radius > 0.0);
		RangeIterator::new(self, pos - Vec2::splat(radius), pos + Vec2::splat(radius))
	}
}

#[cfg(feature = "client")]
impl BlockBorder {
	/**
	 * Returns whether any part of the grid cell at the given position is outside
	 * of the world.
	 */
	pub fn outside(&self, pos: IVec2) -> bool {
		pos.cmplt(self.0).any() || pos.cmpgt(self.1).any()
	}

	pub fn points(&self) -> (IVec2, IVec2) {
		(self.0, self.1)
	}
}

#[cfg(all(test, feature = "client"))]
#[allow(clippy::tests_outside_test_module)] // Not actually a problem
mod tests {
	use super::*;

	fn get(grid_size: UVec2, block_size: u32, world_size: Vec2) -> BlockBorder {
		let blocks = Blocks {
			block_size: block_size.try_into().unwrap(),
			grid_size,
			data: Box::new([]),
		};
		blocks.world_size_to_block_border(world_size)
	}

	#[test]
	fn onebyone() {
		let border = get(UVec2::ONE, 1, Vec2::ONE);
		assert_eq!(border, BlockBorder(IVec2::ZERO, IVec2::ZERO));
		assert!(border.outside(IVec2::ONE));
		assert!(border.outside(IVec2::NEG_ONE));
		assert!(border.outside(IVec2::X));
		assert!(border.outside(IVec2::Y));
		assert!(border.outside(IVec2::NEG_X));
		assert!(border.outside(IVec2::NEG_Y));
		assert!(!border.outside(IVec2::ZERO));
	}

	#[test]
	fn normal() {
		let border = get(UVec2::splat(50), 1, Vec2::splat(50.0));
		assert_eq!(border, BlockBorder(IVec2::ZERO, IVec2::splat(49)));
		assert!(border.outside(IVec2::NEG_ONE));
		assert!(border.outside(IVec2::splat(50)));
		assert!(!border.outside(IVec2::ZERO));
		assert!(!border.outside(IVec2::splat(25)));
	}

	#[test]
	fn different_sizes() {
		let border = get(UVec2::splat(30), 1, Vec2::splat(29.0));
		assert_eq!(border, BlockBorder(IVec2::ONE, IVec2::splat(28)));
		assert!(border.outside(IVec2::ZERO));
		assert!(!border.outside(IVec2::ONE));
	}

	#[test]
	fn empty() {
		for bs in 1..10 {
			let border = get(UVec2::splat(10), bs, Vec2::ZERO);
			assert!(border.0.cmpgt(border.1).any());
			assert!(border.outside(IVec2::ZERO));
		}
	}

	#[test]
	fn negative() {
		for bs in 1..10 {
			let border = get(UVec2::splat(10), bs, Vec2::splat(-5.0));
			assert_eq!(border, BlockBorder(IVec2::MAX, IVec2::MIN));
			assert!(border.outside(IVec2::ZERO));
		}
	}

	#[test]
	fn large_blocks() {
		let border = get(UVec2::splat(25), 2, Vec2::splat(50.0));
		assert_eq!(border, BlockBorder(IVec2::ZERO, IVec2::splat(24)));
	}

	#[test]
	fn large_blocks2() {
		let border = get(UVec2::splat(1), 3, Vec2::splat(1.0));
		assert!(border.outside(IVec2::ZERO));
	}

	#[test]
	fn infinite() {
		for bs in 1..10 {
			let border = get(UVec2::splat(50), bs, Vec2::INFINITY);
			assert_eq!(border, BlockBorder(IVec2::MIN, IVec2::MAX));
			assert!(!border.outside(IVec2::ZERO));
			assert!(!border.outside(IVec2::MIN));
			assert!(!border.outside(IVec2::MAX));
			assert!(!border.outside(IVec2::new(i32::MIN, i32::MAX)));
			assert!(!border.outside(IVec2::new(i32::MAX, i32::MIN)));
		}
	}
}
