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

use glam::Vec2;
use rand::{RngCore, SeedableRng};

use move_chooser::{MoveChooser, MoveHint};
use portal::PortalManager;
use effect_zone::EffectZones;
use flags::Flags;

use super::{joined::{Joined, JoinInfo}, scoring::ScoringEvent, config::EventConfig, super::common::{events::{EffectController, underpower::UnderpowerCommand}, changes::ServerChanges}};

use crate::world::{
	World, WorldRng, event::{WorldEvent, MoveType}, changes::IgnoreChanges,
	player::{PlayerId, PlayerStyle}, ammo_crate::RADIUS as AMMO_CRATE_RADIUS,
	powerup::RADIUS as POWERUP_RADIUS, effects::{Effect, EffectInfo},
	team::TeamId, flag::FlagState, DELTA_TIME, MAX_AMMO_CRATES, MAX_POWERUPS,
};
use crate::protocol::message::text_io::TextIoString;
use crate::utils::{rng::Float, maths::{self, Circle, CollidingRect, RectCorners}};

pub struct EventChooser {
	rng: WorldRng,
	effect_controller: EffectController,
	portal_manager: PortalManager,
	effect_zones: EffectZones,
	flags: Flags,
	config: EventConfig,
	ppssr2: f32, // Player powerup spawn separation radius squared
	pacssr2: f32, // Player ammo crate spawn separation radius squared
}

impl EventChooser {
	pub fn new(portal_manager: PortalManager, effect_zones: EffectZones, flags: Flags, config: EventConfig) -> EventChooser {
		let rng = WorldRng::seed_from_u64(rand::random::<u64>());
		let ppssr2 = config.player_powerup_spawn_separation_radius.powi(2);
		let pacssr2 = config.player_ammo_crate_spawn_separation_radius.powi(2);

		EventChooser {
			rng,
			effect_controller: EffectController::new(config.effects.powerup_effect_reduction),
			portal_manager, effect_zones,
			flags,
			config, ppssr2, pacssr2,
		}
	}

	pub fn underpower_command(&mut self, id: PlayerId, cmd: UnderpowerCommand) -> TextIoString {
		self.effect_controller.underpower_command(id, cmd)
	}

	pub fn get_pre_events(&mut self, world: &World, move_chooser: &dyn MoveChooser, joined: Option<&Joined>) -> Vec<WorldEvent> {
		let mut events = Vec::new();

		/*
		 * Also using the client limits to prevent clients from disconnecting if
		 * there are too many. Not also doing this with bullets as that case is
		 * less likely to occur (this can occur by players staying idle for a
		 * while) and players not being able to shoot would be weird.
		 */
		if self.rng.next_f32() < EventChooser::get_prob(self.config.ammo_crate_period, world.all_player_count()) && world.get_ammo_crates().len() < MAX_AMMO_CRATES {
			let can_spawn = |pos| {
				let rect = RectCorners::new_unchecked(pos - AMMO_CRATE_RADIUS, pos + AMMO_CRATE_RADIUS);
				!(
					world.get_blocks().iter_range(pos, AMMO_CRATE_RADIUS).count() > 0 ||
					self.portal_manager.sink_colliding(&rect) ||
					world.get_players().values().any(|player| pos.distance_squared(player.get_pos()) < self.pacssr2)
				)
			};
			if let Some(pos) = EventChooser::find_item_pos(world, &mut self.rng, &self.config, AMMO_CRATE_RADIUS, can_spawn) {
				events.push(WorldEvent::NewAmmoCrate(pos));
			}
		}

		if self.rng.next_f32() < EventChooser::get_prob(self.config.powerup_period, world.get_players().len()) && world.get_powerups().len() < MAX_POWERUPS && !self.config.allowed_powerups.is_empty() {
			let can_spawn = |pos| {
				let circle = (pos, POWERUP_RADIUS);
				!(
					world.get_blocks().iter_range(pos, POWERUP_RADIUS).any(|block| circle.colliding_rect(block.get_rect())) ||
					self.portal_manager.sink_colliding(&circle) ||
					world.get_players().values().any(|player| pos.distance_squared(player.get_pos()) < self.ppssr2)
				)
			};
			if let Some(pos) = EventChooser::find_item_pos(world, &mut self.rng, &self.config, POWERUP_RADIUS, can_spawn) {
				let effect = self.config.allowed_powerups[self.rng.next_u32() as usize % self.config.allowed_powerups.len()];
				events.push(WorldEvent::NewPowerup(pos, effect));
			}
		}

		if let Some(joined) = joined {
			for (id, info) in joined.players() {
				self.add_player(id, info, world, move_chooser, &mut events);
			}
		}

		events
	}

	pub fn add_player(&self, id: PlayerId, info: JoinInfo<PlayerStyle>, world: &World, move_chooser: &dyn MoveChooser, events: &mut Vec<WorldEvent>) {
		events.push(WorldEvent::NewPlayer(id, info.style, info.team, info.config));
		self.reset_player(id, info.team, world, move_chooser, events);
	}

	fn reset_player(&self, player_id: PlayerId, team_id: Option<TeamId>, world: &World, move_chooser: &dyn MoveChooser, events: &mut Vec<WorldEvent>) {
		events.push(WorldEvent::PlayerPos(player_id, self.find_player_pos(MoveType::Spawn, team_id, world, move_chooser), MoveType::Spawn));
		events.push(WorldEvent::PlayerAmmo(player_id, self.config.ammo_count.0));
	}

	fn apply_and_push(world: &mut World, events: &mut Vec<WorldEvent>, event: WorldEvent) {
		world.apply_event(&event, &mut IgnoreChanges);
		events.push(event);
	}

	fn update_effect_controller(&mut self, world: &mut World, events: &mut Vec<WorldEvent>) {
		let mut tmp_events = Vec::new();
		self.effect_controller.update(world, &mut tmp_events, DELTA_TIME);
		for event in tmp_events {
			Self::apply_and_push(world, events, event);
		}
	}

	pub fn get_and_apply_post_events(&mut self, world: &mut World, move_chooser: &dyn MoveChooser, changes: &ServerChanges) -> (Vec<WorldEvent>, Vec<ScoringEvent>) {
		let mut events = Vec::new();

		for (&player_id, &(typ, team_id)) in &changes.randomly_moved {
			if typ == MoveType::Spawn { // Player killed so remove any flags
				if let Some(pos) = world.get_players().get(&player_id).map(Circle::get_pos) {
					let mut tmp_events = Vec::new();
					for (index, flag) in world.get_flags().iter().enumerate() {
						if let FlagState::Following(id, animation) = flag.get_state() && *id == player_id {
							tmp_events.push(WorldEvent::FlagState(index, FlagState::Fixed(pos + animation.get_dpos(0.0))));
						}
					}
					for event in tmp_events { Self::apply_and_push(world, &mut events, event); }
				}
			}

			Self::apply_and_push(world, &mut events, WorldEvent::PlayerPos(player_id, self.find_player_pos(typ, team_id, world, move_chooser), typ));
			self.portal_manager.on_random_move(player_id);
		}

		for &id in &changes.dead {
			Self::apply_and_push(world, &mut events, WorldEvent::PlayerVel(id, Vec2::ZERO));
			Self::apply_and_push(world, &mut events, WorldEvent::PlayerAmmo(id, self.config.ammo_count.0));

			// Resets all effects when a player dies
			self.effect_controller.reset_player_effects(id);
		}

		{
			let mut tmp_events = Vec::new();
			self.portal_manager.events(world, &mut self.rng, &mut tmp_events);
			for event in tmp_events { Self::apply_and_push(world, &mut events, event); }
		}

		for &id in &changes.ammo_crates_collected {
			if changes.dead.contains(&id) { continue; }
			let Some(player) = world.get_players().get(&id) else { continue; };

			let range = self.config.ammo_crate_supply_range;
			let (min, max) = (range.0, range.1);
			let new_ammo = player.get_ammo_count() + (min + self.rng.next_u32() % ((max - min).saturating_add(1)));
			Self::apply_and_push(world, &mut events, WorldEvent::PlayerAmmo(id, new_ammo));
		}

		for &(id, effect) in &changes.effects_to_apply {
			if changes.dead.contains(&id) { continue; }
			let Some(player) = world.get_players().get(&id) else { continue; };

			self.effect_controller.powerup_collected(id, effect, EffectInfo { time: self.config.effects.effect_time, power: self.config.effects.get_effect_power(effect) });

			if effect == Effect::Regeneration {
				Self::apply_and_push(world, &mut events, WorldEvent::PlayerHealth(id, player.add_health(self.config.effects.health_powerup_health_increase)));
			}
		}

		self.effect_zones.update(world, &mut self.effect_controller);
		self.update_effect_controller(world, &mut events);

		let mut scoring_events = Vec::new();
		{
			let mut tmp_events = Vec::new();
			self.flags.update(world, &mut tmp_events, &mut scoring_events);
			for event in tmp_events { Self::apply_and_push(world, &mut events, event); }
		}

		(events, scoring_events)
	}

	pub fn reset_get_and_apply_events(&mut self, world: &mut World, move_chooser: &dyn MoveChooser) -> Vec<WorldEvent> {
		let mut events = Vec::with_capacity(1 + world.get_players().len() * 3);

		Self::apply_and_push(world, &mut events, WorldEvent::ClearItems);
		#[allow(clippy::needless_collect)] // It actually is necessary, you get a compiler error if you remove it
		for (player_id, team_id) in world.get_players().iter().map(|(&id, player)| (id, player.get_team())).collect::<Vec<_>>() {
			Self::apply_and_push(world, &mut events, WorldEvent::PlayerReset(player_id));

			let mut tmp_events = Vec::new();
			self.reset_player(player_id, team_id, world, move_chooser, &mut tmp_events);
			for event in tmp_events {
				Self::apply_and_push(world, &mut events, event);
			}
		}

		self.effect_controller.reset_all_effects();
		self.update_effect_controller(world, &mut events);
		self.portal_manager.reset();

		{
			let mut tmp_events = Vec::new();
			self.flags.reset(world, &mut tmp_events);
			for event in tmp_events { Self::apply_and_push(world, &mut events, event); }
		}

		events
	}

	fn get_prob(mean_interval: f32, players: usize) -> f32 {
		(DELTA_TIME / mean_interval) * players as f32
	}

	fn find_item_pos(world: &World, rng: &mut WorldRng, config: &EventConfig, radius: f32, should_spawn: impl Fn(Vec2) -> bool) -> Option<Vec2> {
		let players = world.get_players();
		let world_size = world.get_size();

		debug_assert!(!players.is_empty()); // Nothing should spawn when there are no players

		for _ in 0..20 { // Gives up if no suitable place is found after too many tries
			// Lazy O(n) algorithm, might improve later
			let rand_player_pos = players.iter().nth(rng.next_u32() as usize % players.len()).unwrap().1.get_pos();

			// Gets the range for spawning, which must be inside the world
			let min = (rand_player_pos - Vec2::splat(config.item_spawn_radius)).max(radius - world_size / 2.0);
			let max = (rand_player_pos + Vec2::splat(config.item_spawn_radius)).min(world_size / 2.0 - radius);

			let pos = Vec2::new(maths::lerp(min.x, max.x, rng.next_f32()), maths::lerp(min.y, max.y, rng.next_f32()));

			// If not colliding with any block (or some other condition)
			if should_spawn(pos) {
				return Some(pos);
			}
		}

		None
	}

	/**
	 * Makes an attempt to not spawn the player on a powerup, but if it no
	 * position is found after too many attempts, it is used anyway.
	 */
	fn find_player_pos(&self, typ: MoveType, team: Option<TeamId>, world: &World, move_chooser: &dyn MoveChooser) -> Vec2 {
		let mut i = 0;
		loop {
			let (pos, hint) = move_chooser.generate(typ, team, world.get_blocks());

			if i >= 20 || hint == Some(MoveHint::Constant) /* Small optimisation if it's known to stay the same */ { return pos; } // Prevents an infinite loop

			if world.get_powerups().iter().any(|powerup| pos.distance_squared(powerup.get_pos()) < self.ppssr2) || world.get_ammo_crates().iter().any(|ac| pos.distance_squared(ac.get_pos()) < self.pacssr2) { i += 1; }
			else { return pos; }
		}
	}
}
