// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
pub mod changes;
pub mod update;
pub mod event;
pub mod blocks;
pub mod special_area;
pub mod player;
pub mod team;
pub mod bullet;
pub mod ammo_crate;
pub mod powerup;
pub mod effects;
pub mod config;
pub mod colour;
pub mod flag;
pub mod checkpoint;

use std::collections::{BTreeMap, BTreeSet};

use serde::{Serialize, Deserialize};
use glam::Vec2;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro128StarStar;

use blocks::Blocks;
use special_area::SpecialArea;
use player::{Player, PlayerId, forcefield::Forcefield};
#[cfg(feature = "client")] use player::PlayerStyle;
use team::{Team, TeamId};
use bullet::BulletManager;
use ammo_crate::AmmoCrate;
use powerup::Powerup;
use effects::{Effect, Effects};
use changes::Changes;
use config::{WorldConfig, MAX_SIZE};
use flag::{Flag, FlagState};
use checkpoint::Checkpoint;

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

pub type WorldRng = Xoshiro128StarStar;

pub const UPDATES_PER_SECOND: f32 = 240.0;
pub const DELTA_TIME: f32 = 1.0 / UPDATES_PER_SECOND;

// At most 60 seconds to avoid DoS attacks clogging up queues
pub const MAX_STEPS: usize = (60.0 * UPDATES_PER_SECOND) as usize;

// Arbitrary limits used by the client to avoid application layer denial of service attacks
#[cfg(feature = "client")] pub const MAX_PLAYERS: usize = 1024;
#[cfg(feature = "client")] pub const MAX_TEAMS: usize = 1024;
#[cfg(feature = "client")] pub const MAX_BULLETS: usize = 16384;
#[cfg(feature = "client")] pub const MAX_SPECIAL_AREAS: usize = 4096;
pub const MAX_AMMO_CRATES: usize = 512;
pub const MAX_POWERUPS: usize = 512;
#[cfg(feature = "client")] const MAX_FLAGS: usize = 512;
#[cfg(feature = "client")] const MAX_CHECKPOINTS: usize = 512;

/**
 * NOTE: All code that is run on both the client and server **must be
 * deterministic**. If not, it's possible that the client's world state gets out
 * of sync of the server state. This could result in, say, a player being at one
 * part of the world on the client and at another part of the world on the
 * server.
 *
 * This means that **anything** that either *is* or *might be* non-deterministic
 * cannot be used.
 *
 * This includes the obvious like calls to `rand::random` but also includes less
 * obvious behaviour like iterating through `HashMap`s or `HashSet`s as the
 * iteration order depends on the random state. In the latter example, to
 * eliminate the probability of accidentally iterating through a `HashMap` or
 * `HashSet`, they **should not** be used in this code.
 *
 * Another thing to watch out for is the use of certain mathematical functions
 * which the Rust documentation states have "unspecified precision" like `sin`
 * and `exp`. Deterministic functions in a crate like `libm` should be used
 * instead. The `glam` crate has been configured to use `libm`.
 *
 * Note that these strict requirements don't exist for code that just runs on
 * the server (like generating events input to the world) or the client (like
 * rendering).
 */
#[derive(Clone, Serialize, Deserialize)]
pub struct World {
	s: WorldStatic,
	d: WorldDynamic,
}

#[derive(Clone, Serialize, Deserialize)]
struct WorldStatic {
	config: WorldConfig,
	blocks: Blocks,
	teams: Box<[Team]>,
	special_areas: Box<[SpecialArea]>,
	checkpoints: Box<[Checkpoint]>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorldDynamic {
	players: BTreeMap<PlayerId, Player>,
	bullets: BulletManager,
	ammo_crates: Vec<AmmoCrate>,
	powerups: Vec<Powerup>,
	effects: Effects,
	rng: WorldRng,
	flags: Box<[Flag]>,
	active_checkpoint: Option<usize>,
}

impl World {
	pub fn new(mut config: WorldConfig, blocks: Blocks, special_areas: Box<[SpecialArea]>, teams: Box<[Team]>, flags: Box<[Flag]>, checkpoints: Box<[Checkpoint]>) -> World {
		config.size = config.size.min(Vec2::splat(MAX_SIZE));
		World {
			s: WorldStatic { config, blocks, teams, special_areas, checkpoints },
			d: WorldDynamic {
				players: BTreeMap::new(),
				bullets: BulletManager::new(),
				ammo_crates: Vec::new(),
				powerups: Vec::new(),
				effects: Effects::new(),
				rng: WorldRng::seed_from_u64(rand::random::<u64>()),
				flags,
				active_checkpoint: None,
			},
		}
	}

	/** Validation that just needs to be performed once. */
	#[cfg(feature = "client")]
	pub fn validate_init(&self) -> Result<(), &'static str> {
		if !self.s.config.size.cmpge(Vec2::splat(0.0)).all() || !self.s.config.size.cmple(Vec2::splat(MAX_SIZE)).all() { return Err("invalid world size"); } // Need to do ! >= rather than <, as the latter allows a world size of NaN
		if self.s.special_areas.len() > MAX_SPECIAL_AREAS { return Err("too many special areas in world"); }
		if self.s.teams.len() > MAX_TEAMS { return Err("too many teams"); }
		if self.d.flags.len() > MAX_FLAGS { return Err("too many flags"); }
		if self.s.checkpoints.len() > MAX_CHECKPOINTS { return Err("too many checkpoints"); }

		self.s.blocks.validate()?;

		Ok(())
	}

	/** Validation that needs to be performed every update. */
	#[cfg(feature = "client")]
	pub fn validate(&self) -> Result<(), &'static str> {
		if self.d.players.len() > MAX_PLAYERS { return Err("too many players in world"); }
		if self.d.bullets.len() > MAX_BULLETS { return Err("too many bullets in world"); }
		if self.d.ammo_crates.len() > MAX_AMMO_CRATES { return Err("too many ammo crates in world"); }
		if self.d.powerups.len() > MAX_POWERUPS { return Err("too many powerups in world"); }
		self.d.effects.validate()
	}

	#[cfg(feature = "client")]
	pub fn set_dynamic_data(&mut self, d: WorldDynamic) {
		self.d = d;
	}

	#[cfg(feature = "client")] pub fn get_config(&self) -> &WorldConfig { &self.s.config }
	pub fn get_size(&self) -> Vec2 { self.s.config.size }
	pub fn get_blocks(&self) -> &Blocks { &self.s.blocks }
	#[cfg(feature = "client")] pub fn get_special_areas(&self) -> &[SpecialArea] { &self.s.special_areas }
	#[cfg(feature = "client")] pub fn get_checkpoints(&self) -> &[Checkpoint] { &self.s.checkpoints }
	pub fn all_player_count(&self) -> usize { self.d.players.len() }
	pub fn human_player_count(&self) -> usize { self.d.players.values().filter(|player| player.is_human()).count() }
	pub fn get_players(&self) -> &BTreeMap<PlayerId, Player> { &self.d.players }
	#[cfg(feature = "client")] pub fn get_bullets(&self) -> &BulletManager { &self.d.bullets }
	#[cfg(feature = "client")] pub fn get_effects(&self) -> &Effects { &self.d.effects }
	pub fn get_ammo_crates(&self) -> &[AmmoCrate] { &self.d.ammo_crates }
	pub fn get_powerups(&self) -> &[Powerup] { &self.d.powerups }
	pub fn get_flags(&self) -> &[Flag] { &self.d.flags }
	pub fn get_dynamic_data(&self) -> &WorldDynamic { &self.d }

	pub fn get_forcefields(&self) -> Vec<Forcefield> {
		self.d.effects
			.player_iter_with_effect(Effect::Forcefield)
			.map(|(player_id, info)| {
				let player = &self.d.players[&player_id];
				Forcefield { player_id, team_id: player.get_team(), pos: player.get_pos(), strength: info.power }
			})
			.collect()
	}

	pub fn has_player(&self, id: PlayerId) -> bool { self.d.players.contains_key(&id) }
	pub fn player_id_iter(&self) -> impl Iterator<Item = PlayerId> + '_ { self.d.players.keys().copied() }
	pub fn team_id_iter(&self) -> impl Iterator<Item = TeamId> + '_ { (0..self.s.teams.len()).map(TeamId::from_index) }

	fn remove_players(&mut self, ids: BTreeSet<PlayerId>, changes: &mut impl Changes) {
		if ids.is_empty() { return; }

		for flag in &mut self.d.flags {
			if let FlagState::Following(id, animation) = flag.get_state() && ids.contains(id) {
				flag.set_state(FlagState::Fixed(self.d.players.get(id).map_or(Vec2::NAN, Circle::get_pos) + animation.get_dpos(0.0)), changes, |_| None /* Falls back to the same position */);
			}
		}

		for &id in &ids {
			self.d.players.remove(&id);
			self.d.effects.remove_player(id);
			self.d.bullets.disown(id);
		}

		changes.removed_players(ids);
	}

	#[cfg(feature = "client")]
	pub fn get_player_style(&self, id: PlayerId) -> Option<PlayerStyle> {
		self.d.players.get(&id).map(Player::get_style)
	}

	/**
	 * Returns Some(id) of the team the next player should join if teams are
	 * enabled, otherwise None if there are no teams.
	 */
	pub fn next_team(&self) -> Option<TeamId> {
		self.team_freqs().into_iter().enumerate().min_by_key(|&(_id, freq)| freq).map(|(id, _freq)| TeamId::from_index(id))
	}

	pub fn has_team(&self, id: TeamId) -> bool {
		id.index() < self.s.teams.len()
	}

	pub fn team_count(&self) -> usize {
		self.s.teams.len()
	}

	pub fn has_teams(&self) -> bool {
		!self.s.teams.is_empty()
	}

	pub fn team_freqs(&self) -> Vec<usize> {
		let mut freqs = vec![0usize; self.s.teams.len()];
		for id in self.d.players.values().filter_map(Player::get_team) {
			if let Some(f) = freqs.get_mut(id.index()) {
				*f += 1;
			}
		}
		freqs
	}

	pub fn get_team(&self, id: TeamId) -> Option<&Team> {
		self.s.teams.get(id.index())
	}

	pub fn get_player_team(&self, id: PlayerId) -> Option<TeamId> {
		self.d.players.get(&id).and_then(Player::get_team)
	}

	#[cfg(feature = "client")]
	pub fn get_active_checkpoint(&self) -> Option<usize> {
		self.d.active_checkpoint
	}

	#[cfg(feature = "client")]
	pub fn get_active_checkpoint_pos(&self) -> Option<Vec2> {
		self.d.active_checkpoint.and_then(|index| self.s.checkpoints.get(index)).map(Checkpoint::get_pos)
	}

	pub fn teams_to_players(&self, teams: BTreeSet<TeamId>) -> BTreeSet<PlayerId> {
		self.d.players
			.iter()
			.filter_map(|(&id, player)| player.get_team().is_some_and(|team| teams.contains(&team)).then_some(id))
			.collect()
	}
}
