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

use serde::{Serialize, Deserialize};

use super::{World, PlayerId, changes::Changes, event::WorldEvent, DELTA_TIME, player::update::PlayerUpdate, bullet::Bullet, effects::Effect};

use crate::utils::maths::{Circle, CollidingCircle, CollidingRect};

#[derive(Default, Clone, Serialize, Deserialize)]
pub struct WorldUpdate {
	pub pre_events: Vec<WorldEvent>,
	pub players: BTreeMap<PlayerId, PlayerUpdate>,
	pub post_events: Vec<WorldEvent>,
}

impl World {
	pub fn update(&mut self, player_updates: &BTreeMap<PlayerId, PlayerUpdate>, changes: &mut impl Changes) {
		// Removes all players not in the update
		let old_ids = self.d.players.keys().copied().collect::<BTreeSet<_>>().difference(&player_updates.keys().copied().collect()).copied().collect::<BTreeSet<_>>();
		self.remove_players(old_ids, changes);

		// Moves the players
		for (&id, &player_update) in player_updates {
			if let Some(player) = self.d.players.get_mut(&id) {
				player.set_update(player_update);
				player.update(DELTA_TIME, self.s.config.size, &self.s.blocks, &self.d.effects, id);
				player.decrease_cooldown(DELTA_TIME * self.d.effects.get_mul(id, Effect::Reload));
				if player.shoot() {
					self.d.bullets.add(Bullet::new(player, id, self.d.effects.get_mul(id, Effect::Damage), Bullet::random_spread(player, &mut self.d.rng)));
					changes.bullet_shot(id, player, &self.d.effects);
				}
			} else {
				log::warn!("players in update (ids {:?}) don't match players the world (ids {:?})", player_updates.keys().copied().collect::<Vec<_>>(), self.d.players.keys().copied().collect::<Vec<_>>());
			}
		}

		// Updates the rest of the world
		self.d.ammo_crates.retain_mut(|ac| {
			if !ac.update(DELTA_TIME) { return false; }
			if let Some((&id, _)) = self.d.players.iter_mut().find(|(_, player)| player.colliding_rect(ac)) {
				changes.ammo_crate_collected(id, ac.get_pos());
				false
			} else { true }
		});

		changes.clear_powerups();
		self.d.powerups.retain_mut(|powerup| {
			if !powerup.update(DELTA_TIME) { return false; }
			if let Some((&id, player)) = self.d.players.iter().find(|(_, player)| player.colliding_circle(powerup)) {
				changes.powerup_collected(id, player, powerup.get_type(), powerup.get_pos());
				false
			} else { true }
		});
		self.d.effects.update(DELTA_TIME);

		self.d.bullets.update(DELTA_TIME, &self.get_forcefields());

		for (&id, player) in &mut self.d.players {
			let collided = self.d.bullets.collide_player(player, id, self.s.config.friendly_fire);

			/*
			 * Prevents the same player being "killed twice" (having two Hits
			 * with killed = true when two bullets are colliding and the player
			 * is killed). This is important as scorekeeping and spawning
			 * explosion particles rely on there being one Hit with the player
			 * killed.
			 */
			let mut already_killed = false;
			for bullet in collided {
				player.on_bullet_hit(&bullet);
				let killed = player.is_dead();
				changes.add_hit(bullet, player, id, killed && !already_killed);
				already_killed |= killed;
			}

			if player.is_dead() {
				player.reset();
			}
		}

		changes.update(DELTA_TIME);
		for bullet in self.d.bullets.collide_blocks(&self.s.blocks) {
			changes.block_bullet_collision(bullet);
		}

		for flag in &mut self.d.flags {
			flag.update(DELTA_TIME);
		}

		for (i, checkpoint) in self.s.checkpoints.iter().enumerate() {
			if self.d.players.values().any(|player| player.is_human() && player.colliding_circle(checkpoint)) {
				if self.d.active_checkpoint != Some(i) {
					changes.checkpoint_activated(checkpoint.get_pos());
				}
				self.d.active_checkpoint = Some(i);
				break;
			}
		}
	}
}
