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

use glam::Vec2;

use crate::app::playing::view::Sfx;
use crate::world::{
	World, changes::Changes, player::{Player, PlayerId}, bullet::Bullet,
	powerup::PowerupType, effects::{Effect, Effects, EffectInfo},
	event::MoveType, team::TeamId, colour::Colour, player::{PlayerTexture,
	direction::Direction},
};
use crate::utils::maths::Circle;

pub struct Hit {
	pub bullet: Bullet,
	pub player_id: Option<PlayerId>,
	pub player_pos: Vec2,
	pub player_vel: Vec2,
	pub player_dir: Direction,
	pub player_colour: Colour,
	pub player_texture: PlayerTexture,
	pub killed: bool,
}

pub struct ClientChanges { // Used by the clients
	current_id: PlayerId,
	prev_shooting: BTreeSet<PlayerId>,
	collected_powerups: BTreeSet<(PlayerId, PowerupType)>,
	new_players: BTreeSet<PlayerId>,
	pub update_time: f32,
	pub moved: BTreeSet<PlayerId>, // Whenever the server moves a player, which can be on death or teleportation
	pub hits: Vec<Hit>,
	pub reset: BTreeSet<PlayerId>,
	pub items_reset: bool,
	pub current_team_change: Option<TeamId>,
	pub sounds: Vec<(Sfx, Vec2, Vec2)>,
	pub powerup_sounds: Vec<(PlayerId, PowerupType, Vec2)>,
}

impl ClientChanges {
	pub fn new(world: &World, current_id: PlayerId) -> ClientChanges {
		ClientChanges {
			current_id,
			prev_shooting: world.get_players().iter().filter_map(|(id, player)| player.get_input_state().shooting().then_some(*id)).collect(),
			collected_powerups: BTreeSet::new(),
			new_players: BTreeSet::new(),
			update_time: 0.0,
			moved: BTreeSet::new(),
			hits: Vec::new(),
			reset: BTreeSet::new(),
			items_reset: false,
			current_team_change: None,
			sounds: Vec::new(),
			powerup_sounds: Vec::new(),
		}
	}

	/**
	 * Need to also check the time condition.
	 */
	pub fn shot_not_predicted(&self, bullet: &Bullet) -> bool {
		bullet.get_player_owner().is_none_or(|id| id != self.current_id && !self.prev_shooting.contains(&id))
	}
}

impl Changes for ClientChanges {
	fn removed_players(&mut self, old_ids: BTreeSet<PlayerId>) {
		for id in &old_ids {
			self.moved.remove(id);
			self.reset.remove(id);
			self.prev_shooting.remove(id);
			self.new_players.remove(id);
		}

		self.collected_powerups.retain(|(id, _typ)| !old_ids.contains(id));

		for hit in &mut self.hits {
			if let Some(id) = hit.player_id && old_ids.contains(&id) {
				hit.player_id = None;
			}
		}
	}

	fn update(&mut self, dt: f32) {
		self.update_time += dt;
	}

	fn new_player(&mut self, id: PlayerId) {
		self.new_players.insert(id);
	}

	fn bullet_shot(&mut self, id: PlayerId, player: &Player, effects: &Effects) {
		// Only when the client cannot predict the next shots
		if id != self.current_id && !self.prev_shooting.contains(&id) {
			self.sounds.push((Sfx::Shot(effects.get_mul(id, Effect::Reload)), player.get_pos(), player.get_vel()));
		}
	}

	fn add_hit(&mut self, bullet: Bullet, player: &Player, player_id: PlayerId, killed: bool) {
		self.hits.push(Hit {
			bullet,
			player_id: Some(player_id),
			player_pos: player.get_pos(),
			player_vel: player.get_vel(),
			player_dir: player.get_dir(),
			player_colour: player.get_colour(),
			player_texture: player.get_texture(),
			killed,
		});
	}

	fn block_bullet_collision(&mut self, bullet: Bullet) {
		/*
		 * Only from bullets that were shot (and the client couldn't predict the
		 * shot) and collided with a block too quickly for the smooth world to
		 * clone them.
		 */
		if self.shot_not_predicted(&bullet) && bullet.get_time() <= self.update_time {
			self.sounds.push((Sfx::BlockBullet, bullet.get_pos(), Vec2::ZERO));
		}
	}

	fn pos_changed(&mut self, id: PlayerId, typ: MoveType, prev_pos: Vec2) {
		self.moved.insert(id);

		if typ == MoveType::Teleportation && !self.collected_powerups.contains(&(id, PowerupType::Teleportation)) {
			self.powerup_sounds.push((id, PowerupType::Teleportation, prev_pos));
		}
	}

	fn ammo_crate_collected(&mut self, _id: PlayerId, pos: Vec2) {
		self.sounds.push((Sfx::AmmoCrate, pos, Vec2::ZERO));
	}

	fn clear_powerups(&mut self) {
		self.collected_powerups.clear();
	}

	fn powerup_collected(&mut self, id: PlayerId, _player: &Player, typ: PowerupType, pos: Vec2) {
		self.collected_powerups.insert((id, typ));
		self.powerup_sounds.push((id, typ, pos));
	}

	fn effect_event(&mut self, id: PlayerId, effect: Effect, mut old_info: EffectInfo, mut new_info: EffectInfo, player_pos: Vec2, play_sound: bool) {
		if !play_sound { return; }

		/*
		 * Prevents the sound from playing whenever players join the game, which
		 * can happen if an effect zone covers the entire map.
		 */
		if self.new_players.contains(&id) { return; }

		/*
		 * If this `PlayerEffect` event is because of a powerup collection, a
		 * sound was already pushed so don't push another one.
		 */
		let typ = PowerupType::from(effect);
		if self.collected_powerups.contains(&(id, typ)) { return; }

		old_info.normalise(effect);
		new_info.normalise(effect);

		// If the effect is better
		if new_info.power > old_info.power {
			self.powerup_sounds.push((id, typ, player_pos));
		}
	}

	fn flag_captured(&mut self, pos: Vec2) {
		self.sounds.push((Sfx::FlagCapture, pos, Vec2::ZERO));
	}

	fn flag_dropped(&mut self, pos: Vec2) {
		self.sounds.push((Sfx::FlagDrop, pos, Vec2::ZERO));
	}

	fn player_reset(&mut self, id: PlayerId) {
		self.reset.insert(id);
	}

	fn items_reset(&mut self) {
		self.items_reset = true;
	}

	fn player_team_change(&mut self, player_id: PlayerId, team_id: Option<TeamId>) {
		if player_id == self.current_id {
			self.current_team_change = team_id.or(self.current_team_change);
		}
	}

	fn checkpoint_activated(&mut self, pos: Vec2) {
		self.sounds.push((Sfx::Checkpoint, pos, Vec2::ZERO));
	}
}
