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

use serde::{Serialize, Deserialize};
use glam::Vec2;

use super::{
	World, changes::Changes, colour::Colour, player::{Player, PlayerId,
	PlayerStyle, PlayerName, ammo_count::AmmoCount, config::PlayerConfig,
	direction::Direction}, ammo_crate::AmmoCrate, powerup::{Powerup,
	PowerupType}, effects::{Effect, EffectInfo}, team::TeamId, flag::FlagState,
};

use crate::utils::maths::Circle;

#[derive(Clone, Serialize, Deserialize)]
pub enum WorldEvent {
	/// When a new player joins.
	NewPlayer(PlayerId, PlayerStyle, Option<TeamId>, Option<PlayerConfig>),

	/// When a player is removed. Most of the time when a player is removed, this
	/// event isn't emitted but rather the player id isn't included in the update
	/// map.
	///
	/// This event is useful when removing a player after an update has completed
	/// but that player should exist during the update.
	RemovePlayer(PlayerId),

	/// Calls `Player::reset`, which reset's the health and attack cooldown.
	PlayerReset(PlayerId),

	/// Sets the player's position.
	PlayerPos(PlayerId, Vec2, MoveType),

	/// Sets the player's velocity.
	PlayerVel(PlayerId, Vec2),

	/// Sets the player's direction.
	PlayerDir(PlayerId, Direction),

	/// Sets the player's amount of ammo.
	PlayerAmmo(PlayerId, AmmoCount),

	/// Sets the player's health.
	PlayerHealth(PlayerId, f32),

	/// Adds an effect to a player.
	PlayerEffect(PlayerId, Effect, EffectInfo, bool /* Whether a sound should play */),

	/// Changes the player's name.
	PlayerName(PlayerId, PlayerName),

	/// Changes the player's colour.
	PlayerColour(PlayerId, Colour),

	/// Changes the player's teams.
	PlayerTeam(PlayerId, Option<TeamId>),

	/// Adds a new ammo crate. The amount of ammo is determined when the player
	/// picks it up, in which the server may give the player ammo with the
	/// `PlayerAmmo` update.
	NewAmmoCrate(Vec2),

	/// Adds a new powerup.
	NewPowerup(Vec2, PowerupType),

	/// Flag state changes, either being captured by a player or released. The
	/// first field is the flag's index.
	FlagState(usize, FlagState),

	/// Removes all bullets, ammo crates and powerups from the world.
	ClearItems,

	/// For non-breaking extensions to the protocol without updating the SERP
	/// version.
	Extension(Box<[u8]>),
}

#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MoveType {
	/**
	 * New players spawning into the game and players respawning.
	 */
	Spawn,

	/**
	 * Teleportation powerup.
	 */
	Teleportation,
}

impl World {
	pub fn apply_event(&mut self, event: &WorldEvent, changes: &mut impl Changes) {
		match *event {
			WorldEvent::NewPlayer(id, ref style, team, ref config) => {
				self.d.players.insert(id, Player::new(config.clone().unwrap_or_else(|| self.s.config.default_player.clone()), style.clone(), team));
				changes.new_player(id);
				changes.player_team_change(id, team);
			},
			WorldEvent::RemovePlayer(id) => {
				let mut ids = BTreeSet::new();
				ids.insert(id);
				self.remove_players(ids, changes);
			},
			WorldEvent::PlayerReset(id) => {
				self.player_present(id, Player::reset);
				changes.player_reset(id);
			},
			WorldEvent::PlayerPos(id, pos, typ) => {
				self.player_present(id, |player| {
					changes.pos_changed(id, typ, player.get_pos());
					player.set_pos(pos);
				});
			},
			WorldEvent::PlayerVel(id, vel) => self.player_present(id, |player| player.set_vel(vel)),
			WorldEvent::PlayerDir(id, dir) => self.player_present(id, |player| player.set_dir(dir)),
			WorldEvent::PlayerHealth(id, health) => self.player_present(id, |player| player.set_health(health)),
			WorldEvent::PlayerAmmo(id, ammo) => self.player_present(id, |player| player.set_ammo_count(ammo)),
			WorldEvent::PlayerEffect(id, effect, info, play_sound) => {
				if let Some(player) = self.d.players.get_mut(&id) { // Not using `player_present` because of the borrow checker
					self.d.effects.add_effect(id, effect, info, changes, player.get_pos(), play_sound);
				}
			}
			WorldEvent::PlayerName(id, ref name) => self.player_present(id, |player| player.get_style_mut().set_name(name.clone())),
			WorldEvent::PlayerColour(id, colour) => self.player_present(id, |player| player.get_style_mut().set_colour(colour)),
			WorldEvent::PlayerTeam(player_id, team_id) => {
				self.player_present(player_id, |player| player.set_team(team_id));
				changes.player_team_change(player_id, team_id);
			},
			WorldEvent::NewAmmoCrate(pos) => self.d.ammo_crates.push(AmmoCrate::new(pos)),
			WorldEvent::NewPowerup(pos, typ) => self.d.powerups.push(Powerup::new(pos, typ, self.s.config.powerup_time)),
			WorldEvent::FlagState(index, ref state) => {
				if let Some(flag) = self.d.flags.get_mut(index) {
					flag.set_state(state.clone(), changes, |id| self.d.players.get(&id).map(Circle::get_pos));
				}
			},
			WorldEvent::ClearItems => {
				self.d.bullets.clear();
				self.d.ammo_crates.clear();
				self.d.powerups.clear();
				changes.items_reset();
			},
			WorldEvent::Extension(_) => (),
		}
	}

	pub fn apply_events(&mut self, events: &[WorldEvent], changes: &mut impl Changes) {
		for event in events {
			self.apply_event(event, changes);
		}
	}

	fn player_present(&mut self, id: PlayerId, mut f: impl FnMut(&mut Player)) {
		if let Some(player) = self.d.players.get_mut(&id) {
			f(player);
		}
	}
}
