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

use crate::server::multiplayer::{config::FlagConfig, scoring::ScoringEvent};
use crate::world::{World, event::WorldEvent, flag::{Flag, FlagState, CaptureAnimation}, player::PlayerId, team::TeamId};
use crate::utils::maths::{Circle, RectCorners, CollidingRect, ContainingRect};

pub struct Flags {
	config: Vec<FlagConfig>,
	team_bases: Option<Box<[Option<RectCorners>]>>,

	/*
	 * When a player drops a flag, their id gets added into this set to prevent
	 * the recapture of that flag (or other flags) in later updates.
	 *
	 * They are only removed from the set when they aren't colliding with any
	 * flags.
	 */
	disable_capture: BTreeSet<PlayerId>,
}

impl Flags {
	pub fn new(config: Vec<FlagConfig>, team_bases: Box<[Option<RectCorners>]>) -> Flags {
		let has_bases = team_bases.iter().any(Option::is_some);
		Flags { config, team_bases: has_bases.then_some(team_bases), disable_capture: BTreeSet::new() }
	}

	pub fn reset(&mut self, world: &World, events: &mut Vec<WorldEvent>) {
		self.disable_capture.clear();

		for (index, (flag, config)) in world.get_flags().iter().zip(self.config.iter()).enumerate() {
			Self::reset_flag(index, flag, config, events);
		}
	}

	fn reset_flag(index: usize, flag: &Flag, config: &FlagConfig, events: &mut Vec<WorldEvent>) {
		if flag.get_fixed_pos() != Some(config.default_pos) { // Only pushes an event if the position is different to minimise bandwidth
			events.push(WorldEvent::FlagState(index, FlagState::Fixed(config.default_pos)));
		}
	}

	pub fn update(&mut self, world: &World, events: &mut Vec<WorldEvent>, scoring_events: &mut Vec<ScoringEvent>) {
		let mut capturing_players = world.get_flags().iter().filter_map(Flag::get_following).collect::<BTreeSet<_>>();
		let mut colliding = BTreeSet::new();

		for (index, (flag, config)) in world.get_flags().iter().zip(self.config.iter()).enumerate() {
			match flag.get_state() {
				FlagState::Fixed(pos) => {
					let mut can_capture = Vec::new();
					let mut can_capture_team = Vec::new();

					for (&id, player) in world.get_players() {
						if player.colliding_rect(&Flag::rect_from_pos(*pos)) {
							colliding.insert(id);

							let flag_owned = player.get_team() == Some(config.team);

							if
								!capturing_players.contains(&id) && // Don't want players capturing more than one flag at a time, that would be too OP
								!player.get_input_state().dropping_flag() && // It would feel weird to pick up the flag for one update to then drop it immediately after
								!self.disable_capture.contains(&id) && // Prevents recapture of recently dropped flags while still colliding
								(*pos != config.default_pos || !flag_owned) // Prevents players from capturing their own flags secured at their default position
							{
								let player_pos = player.get_pos();
								can_capture.push((id, player_pos));
								if flag_owned {
									can_capture_team.push((id, player_pos));
								}
							}
						}
					}

					/*
					 * When there are multiple players that can capture a given flag,
					 * randomness is used to determine who gets it, with
					 * prioritisation of players on the flag's team.
					 *
					 * This avoids bias towards the lowest id and keeps things
					 * predictable about which team gets it (if one of those players
					 * is from the flag's team).
					 *
					 * I could also avoid lowest id bias in other areas, such as
					 * bullet collision detection...
					 */
					let mut capture = |(id, player_pos)| {
						capturing_players.insert(id);
						events.push(WorldEvent::FlagState(index, FlagState::Following(id, CaptureAnimation::new(*pos - player_pos))));
					};

					if !can_capture_team.is_empty() {
						capture(can_capture_team[rand::random::<usize>() % can_capture_team.len()]);
					} else if !can_capture.is_empty() {
						capture(can_capture[rand::random::<usize>() % can_capture.len()]);
					}
				},
				FlagState::Following(id, animation) => {
					if let Some(player) = world.get_players().get(id) && player.get_input_state().dropping_flag() {
						/*
						 * Not having the dropping of flags built into the protocol to
						 * give freedom to later protocol-compatible implementations
						 * that disable the dropping of flags.
						 */
						//capturing_players.remove(id); // Don't need to do as players won't be able to recapture any other flags
						self.disable_capture.insert(*id);
						colliding.insert(*id); // Now colliding with the dropped flag
						events.push(WorldEvent::FlagState(index, FlagState::Fixed(player.get_pos() + animation.get_dpos(0.0))));
					}
				},
			}

			if let Some(bases) = &self.team_bases {
				let pos = flag.get_pos(|id| world.get_players().get(&id).map(Circle::get_pos), 0.0);

				for (team, base) in bases.iter().enumerate().filter_map(|(index, base)| base.as_ref().map(|base| (TeamId::from_index(index), base))) {
					if base.contains_rect(&Flag::rect_from_pos(pos)) {
						if
							team == config.team && // Flag returned to own base
							flag.get_following().is_none_or(|id| world.get_players().get(&id).is_none_or(|player| player.get_team() == Some(config.team))) // Don't want flags stuck to the base if captured by an opposing player
						{
							Self::reset_flag(index, flag, config, events);
						} else if team != config.team { // Flag returned to another base
							Self::reset_flag(index, flag, config, events);
							scoring_events.push(ScoringEvent::Flag(team, index));
						}
					}
				}
			}
		}

		self.disable_capture.retain(|id| colliding.contains(id));
	}
}
