// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
pub mod config;
pub mod connection_listener;
mod events;
mod update;
pub mod scoring;
mod command;
pub mod joined;
mod lobby;
mod win_messages;

use std::{fmt::Write, path::PathBuf, collections::BTreeMap, num::NonZeroUsize, time::{Instant, Duration}, sync::Arc, mem};

use tokio::{time, sync::mpsc::Receiver};

use config::Config;
use connection_listener::ConnectionListener;
use events::{EventChooser, move_chooser::{MoveChooser, PlayingMoveChooser, LobbyMoveChooser}, portal::PortalManager, effect_zone::EffectZones, flags::Flags};
use scoring::Scoring;
use lobby::Lobby;
use joined::JoinInfo;

use super::common::{SANITY_CHECK_PERIOD, player_update_queue::PlayerUpdateQueue, changes::ServerChanges};

use crate::net::server::client_count_notify::ClientCountNotify;
use crate::protocol::message::{Message, MiscMessage, text_io::{self, TextIoString, ServerTextOutput}, timer::Timer, stream::{DynMs, MsError}};
use crate::world::{World, event::WorldEvent, player::{PlayerId, HumanStyle}, team::{Team, TeamId}, flag::Flag, special_area::SpecialArea};
use crate::utils::{Clock, maths::RectCorners};

pub struct Server {
	clients: BTreeMap<PlayerId, Client>,
	connection_listener: ConnectionListener,

	world: World,
	max_clients: usize,
	instructions: Option<TextIoString>,
	event_chooser: EventChooser,
	playing_move_chooser: PlayingMoveChooser,
	scoring: Scoring,
	clock: Clock,
	prev_changes: ServerChanges,

	lobby: Lobby,
	can_join_midgame: bool,
	timer: Option<Timer>,
	playing_timer: Option<Timer>,
	sync_timer: bool,
	team_info: TeamInfo,

	next_sanity_check: f32,
}

pub enum ServerMessage {
	AdminMessage(TextIoString),
}

struct TeamInfo {
	map: BTreeMap<Arc<str>, TeamId>,
	shortest_aliases: Box<[Option<Arc<str>>]>,
}

impl TeamInfo {
	fn teams_command_output(&self, player_id: PlayerId, current_team: Option<TeamId>, world: &World) -> TextIoString {
		if world.has_teams() {
			/*
			 * Note that I can use `client.join_info.team` but this can be
			 * Some(...) if the player is spectating (the next team they will
			 * join).
			 *
			 * However, the number of players in each team that this command
			 * reports is the number of players in the world, not the number of
			 * clients (cannot easily do the latter as the borrow checker
			 * complains), so I want to keep things consistent.
			 */
			let mut s = String::new();
			for (index, freq) in world.team_freqs().into_iter().enumerate() {
				if index > 0 {
					s.push('\n');
				}

				let team_id = TeamId::from_index(index);
				s.push_str(world.get_team(team_id).unwrap().get_name());
				if let Some(Some(alias)) = self.shortest_aliases.get(index) {
					let _ = write!(s, " (alias = \"{alias}\")");
				}

				let _ = write!(s, ": {freq} player");
				if freq != 1 {
					s.push('s');
				}

				if Some(team_id) == current_team {
					if world.has_player(player_id) {
						s.push_str(" (including you)");
					} else {
						s.push_str(" (your team)");
					}
				}
			}

			TextIoString::new_truncate(s)
		} else {
			text_io::from_const!("Teams are not enabled.")
		}
	}
}

struct Client {
	stream: Box<DynMs>,
	to_remove: bool,
	spectating: Option<SpectatingMode>,
	join_info: JoinInfo<HumanStyle>,

	/*
	 * All per-player messages are sent in the order they are pushed.
	 *
	 * The usize for each per-player message is an index to a message in the
	 * all-players message list, with this per-player message being sent out
	 * before that all-players message.
	 */
	per_player_messages: Vec<(usize, Message)>,
	updates: PlayerUpdateQueue,
	allowed_chats: f32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SpectatingMode {
	/**
	 * The user explicitly chose to be spectating the game, either through
	 * running the `spectate` command or joining the game as a spectator.
	 */
	Manual,

	/**
	 * The user was put into spectator mode by being eliminated, or joined a game
	 * as a player where players cannot join mid-game.
	 */
	Automatic,
}

impl Client {
	fn new(stream: Box<DynMs>, spectating: Option<SpectatingMode>, join_info: JoinInfo<HumanStyle>) -> Client {
		Client {
			stream,
			to_remove: false,
			spectating,
			join_info,
			per_player_messages: Vec::new(),
			updates: PlayerUpdateQueue::new(),
			allowed_chats: CHAT_MAX_BURST,
		}
	}

	/**
	 * Inserts the given messages to the front of the per-player messages. Useful
	 * for ensuring that the `InitWorld` message is first received, which the
	 * client expects.
	 */
	fn set_init_per_player_messages(&mut self, messages: impl IntoIterator<Item = Message>) {
		self.per_player_messages.splice(..0, messages.into_iter().map(|msg| (0, msg))); // https://stackoverflow.com/a/47039490
	}

	fn send_messages(&mut self, messages: &[Message]) -> Result<(), MsError> {
		let mut per_player_messages = mem::take(&mut self.per_player_messages).into_iter().peekable();

		for (i, msg) in messages.iter().enumerate() {
			while let Some(ppm) = per_player_messages.next_if(|ppm| ppm.0 <= i) {
				self.stream.send(ppm.1)?;
			}

			self.stream.send(msg.clone())?;
		}


		// All per-player messages that are after the final all-players message
		for ppm in per_player_messages {
			self.stream.send(ppm.1)?;
		}

		self.stream.flush()
	}

	fn get_name(clients: &BTreeMap<PlayerId, Client>, id: PlayerId) -> &str {
		clients.get(&id).map_or("[unknown]", |client| client.join_info.style.name.get())
	}
}

const CHAT_RATE_LIMIT: f32 = 2.0; // Chats per second
const CHAT_MAX_BURST: f32 = 10.0;

impl Server {
	pub fn try_new(connection_listener: ConnectionListener, config: Config, config_path: PathBuf, sanity_checks: bool) -> Result<Server, String> {
		config.validate().map_err(|err| format!("invalid config: {err}"))?;

		let blocks = config.blocks.generate(config_path).map_err(|err| format!("cannot generate block map: {err}"))?;

		let custom_spawn_points = config.teams
			.iter()
			.enumerate()
			.filter_map(|(index, team)| team.spawn_point.map(|pos| (TeamId::from_index(index), pos)))
			.collect();

		let mut special_areas = config.special_areas.areas.
			into_iter()
			.flat_map(|area| SpecialArea::rect(RectCorners::new(area.pos, area.pos + area.size), area.colour).into_iter())
			.collect();

		let (teams, team_info, team_bases) = Config::get_teams(config.teams, &mut special_areas).map_err(|err| format!("invalid teams: {err}"))?;

		let can_join_midgame = config.scoring.can_join_midgame;

		let portal_manager = PortalManager::build(config.special_areas.portals, config.special_areas.portal_default_colours, &mut special_areas).map_err(|err| format!("portals: {err}"))?;
		let effect_zones = EffectZones::new(config.special_areas.effect_zones, &config.events, config.special_areas.effect_zone_default_colours, &mut special_areas);
		let scoring = Scoring::new(config.scoring, &teams, &mut special_areas);

		special_areas.retain(|area| !area.get_colour().fully_transparent()); // Fully transparent special areas are useless, so don't send them

		let flags = config.flags
			.iter()
			.map(|flag| Flag::new(flag.default_pos, flag.colour.get(teams.get(flag.team.index()).map(Team::get_colour))))
			.collect();

		let world = World::new(config.world, blocks, special_areas.into_boxed_slice(), teams, flags, Box::from([]));

		let flags = Flags::new(config.flags, team_bases);

		let playing_move_chooser = PlayingMoveChooser::build(&world, config.events.player_spawn_radius, custom_spawn_points)?;
		let lobby_move_chooser = LobbyMoveChooser::build(&world, config.events.player_spawn_radius, config.lobby.local_spawn_radius)?;
		let event_chooser = EventChooser::new(portal_manager, effect_zones, flags, config.events);
		let lobby = Lobby::new(config.lobby, lobby_move_chooser);

		let playing_timer = config.timer;
		let timer = if lobby.active() { lobby.get_timer() } else { playing_timer.clone() };

		Ok(Server {
			clients: BTreeMap::new(),
			connection_listener,
			world,
			max_clients: config.max_clients.map_or(usize::MAX, NonZeroUsize::get),
			instructions: config.instructions,
			event_chooser,
			playing_move_chooser,
			scoring,
			clock: Clock::new_world(),
			prev_changes: ServerChanges::default(),
			lobby,
			can_join_midgame,
			timer,
			playing_timer,
			sync_timer: true,
			team_info,
			next_sanity_check: if sanity_checks { SANITY_CHECK_PERIOD } else { f32::INFINITY },
		})
	}

	pub async fn run(mut self, mut receiver: Receiver<ServerMessage>, mut client_count_notify: ClientCountNotify) {
		const PERIOD: Duration = Duration::from_nanos(10_416_667); // 1/96 seconds

		let mut prev_time = Instant::now();
		let mut prev_client_count = 0;
		let mut dt = 0.0;

		loop {
			let client_count = self.clients.len();
			let player_count = self.world.human_player_count();
			debug_assert!(player_count <= client_count);

			client_count_notify.update(player_count, client_count.saturating_sub(player_count), dt);

			let mut messages = Vec::new();
			while let Ok(msg) = receiver.try_recv() {
				match msg {
					ServerMessage::AdminMessage(msg) => messages.push(Message::Misc(MiscMessage::ServerTextOutput(ServerTextOutput::Admin(msg)))),
				}
			}

			let cur_time = Instant::now();
			dt = cur_time.duration_since(prev_time).as_secs_f32();
			prev_time = cur_time;

			if self.update(dt, messages).is_err() {
				for client in self.clients.into_values() {
					client.stream.close();
				}

				return;
			}

			if prev_client_count != client_count {
				log::info!("client count = {client_count}");
				prev_client_count = client_count;
			}

			time::sleep(PERIOD.saturating_sub(cur_time.elapsed())).await;
		}
	}

	fn reset(&mut self) -> Vec<WorldEvent> {
		// Would like to de-duplicate this code with a method but the borrow checker doesn't allow that
		let move_chooser: &dyn MoveChooser = if self.lobby.active() { self.lobby.get_move_chooser() } else { &self.playing_move_chooser };
		self.event_chooser.reset_get_and_apply_events(&mut self.world, move_chooser)
	}

	fn can_join(&self) -> bool {
		self.lobby.active() || self.can_join_midgame
	}

	fn set_timer(&mut self, timer: Option<Timer>) {
		self.timer = timer;
		self.sync_timer = true;
	}

	fn unused_player_id(&self) -> PlayerId {
		PlayerId::find_unused(|id| !self.clients.contains_key(&id))
	}
}
