// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::collections::BTreeMap;

use glam::Vec2;

use crate::world::{World, event::MoveType, player::{Player, RADIUS}, team::TeamId, blocks::Blocks};

const FALLBACK_MOVES: usize = 256;
const MAX_GEN_ATTEMPTS: usize = 256;
const MAX_INIT_GEN_ATTEMPTS: usize = FALLBACK_MOVES * MAX_GEN_ATTEMPTS * 4;
static ERROR: &str = "fallback move generation taking too long, make sure the player can actually spawn outside of a block near the origin";

pub trait MoveChooser {
	fn generate(&self, typ: MoveType, team: Option<TeamId>, blocks: &Blocks) -> (Vec2, Option<MoveHint>);
}

#[derive(PartialEq, Eq)]
pub enum MoveHint {
	/**
	 * Calling `generate` again won't get a different result.
	 */
	Constant,
}

pub struct PlayingMoveChooser {
	bounds: Vec2,
	fallback: Box<[Vec2]>,
	custom_spawn_points: BTreeMap<TeamId, Vec2>,
}

impl PlayingMoveChooser {
	pub fn build(world: &World, spawn_radius: f32, custom_spawn_points: BTreeMap<TeamId, Vec2>) -> Result<PlayingMoveChooser, String> {
		let bounds = get_bounds(world, spawn_radius);
		let mut moves = Vec::with_capacity(FALLBACK_MOVES);

		let mut i = 0;
		while moves.len() < FALLBACK_MOVES {
			if let Some(mov) = try_generate(bounds, world.get_blocks()) {
				moves.push(mov);
			}

			i += 1;
			if i > MAX_INIT_GEN_ATTEMPTS {
				return Err(String::from(ERROR));
			}
		}

		Ok(PlayingMoveChooser { bounds, fallback: moves.into_boxed_slice(), custom_spawn_points })
	}
}

impl MoveChooser for PlayingMoveChooser {
	fn generate(&self, typ: MoveType, team: Option<TeamId>, blocks: &Blocks) -> (Vec2, Option<MoveHint>) {
		if typ == MoveType::Spawn && let Some(id) = team && let Some(&pos) = self.custom_spawn_points.get(&id) {
			return (pos, Some(MoveHint::Constant));
		}

		for _ in 0..MAX_GEN_ATTEMPTS {
			if let Some(mov) = try_generate(self.bounds, blocks) {
				return (mov, None);
			}
		}

		(self.fallback[rand::random::<usize>() % self.fallback.len()], None)
	}
}

pub struct LobbyMoveChooser {
	bounds: Vec2,
	pos: Vec2,
	radius: f32,
}

impl LobbyMoveChooser {
	pub fn build(world: &World, global_spawn_radius: f32, local_spawn_radius: f32) -> Result<LobbyMoveChooser, String> {
		let bounds = get_bounds(world, global_spawn_radius);
		LobbyMoveChooser::find_pos(bounds, world.get_blocks()).map_or_else(|| Err(String::from(ERROR)), |pos| Ok(LobbyMoveChooser { bounds, pos, radius: local_spawn_radius }))
	}

	pub fn update_spawn_pos(&mut self, blocks: &Blocks) {
		if let Some(pos) = LobbyMoveChooser::find_pos(self.bounds, blocks) {
			self.pos = pos;
		}
	}

	fn find_pos(bounds: Vec2, blocks: &Blocks) -> Option<Vec2> {
		(0..MAX_GEN_ATTEMPTS).find_map(|_| try_generate(bounds, blocks))
	}
}

impl MoveChooser for LobbyMoveChooser {
	fn generate(&self, _typ: MoveType, _team: Option<TeamId>, blocks: &Blocks) -> (Vec2, Option<MoveHint>) {
		((0..20).find_map(|_| validate(self.pos + random_vec2() * self.radius * 2.0, self.bounds, blocks)).unwrap_or(self.pos), None)
	}
}

fn get_bounds(world: &World, spawn_radius: f32) -> Vec2 {
	(world.get_size() / 2.0 - RADIUS).min(Vec2::splat(spawn_radius))
}

fn random_vec2() -> Vec2 {
	rand::random::<Vec2>() - 0.5
}

fn try_generate(bounds: Vec2, blocks: &Blocks) -> Option<Vec2> {
	let pos = random_vec2() * bounds * 2.0;
	validate(pos, bounds, blocks)
}

fn validate(pos: Vec2, bounds: Vec2, blocks: &Blocks) -> Option<Vec2> {
	(!Player::colliding(pos, blocks) && pos.abs().cmple(bounds).all()).then_some(pos)
}
