// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod controller;
pub(super) mod condition;

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

use glam::Vec2;

use controller::Controller;
use condition::AnyCondition;

use super::config::ScoringConfig;

use crate::protocol::message::{Message, WorldMessage, scores::{Scores, DScores}};
use crate::world::{World, player::PlayerId, team::{Team, TeamId}, special_area::SpecialArea};

pub struct Scoring {
	prev_scores: Scores,
	scores: Scores,
	controller: Controller,
	win: AnyCondition,
	elimination: AnyCondition,
	hidden: HiddenScores,
}

pub enum ScoringEvent {
	Kill(PlayerId, Option<TeamId>),
	Death(PlayerId, Option<TeamId>),
	Time(f32),
	Pos(PlayerId, Option<TeamId>, Vec2, f32),
	Flag(TeamId, usize),
}

pub struct ScoringUpdateResult {
	pub winners: Option<Winners>,
	pub eliminated: Option<BTreeSet<PlayerId>>,
}

pub enum Winners {
	Players(BTreeSet<PlayerId>),
	Teams(BTreeSet<TeamId>),
}

/**
 * Configuration for some scores to be stored internally but not sent to the
 * client.
 *
 * Scores are hidden if they can never change, so the scores provide no
 * information and displaying them only clutters the UI.
 */
struct HiddenScores {
	players: bool,
	teams: bool,
}

impl Scoring {
	pub fn new(config: ScoringConfig, teams: &[Team], special_areas: &mut Vec<SpecialArea>) -> Scoring {
		let scores = Scores::new(config.player_ordering, config.team_ordering);
		let (controller, hidden) = Controller::new(config.controller, teams, special_areas);
		Scoring {
			prev_scores: scores.clone(),
			scores,
			controller,
			win: config.win,
			elimination: config.elimination,
			hidden,
		}
	}

	/**
	 * Clears all scores.
	 *
	 * Should be called whenever the game finishes and enters the lobby state.
	 */
	pub fn clear(&mut self) {
		self.scores.clear();
	}

	/**
	 * Resets all scores to their initial values.
	 *
	 * Should be called when the game starts.
	 */
	pub fn start(&mut self) {
		self.scores.clear(); // Should already be cleared, but just in case
		self.controller.reset();
		self.win.reset();
		self.elimination.reset();
	}

	pub fn event(&mut self, event: ScoringEvent) {
		self.controller.event(event, &mut self.scores);
	}

	pub fn update(&mut self, world: &World, time: Option<f64>) -> ScoringUpdateResult {
		// Removes old players/teams and adds new players/teams
		self.controller.change_players(&mut self.scores, world);

		let eliminated = match &mut self.elimination {
			AnyCondition::Player(cond) => cond.satisfying(self.scores.get_player_scores(), time),
			AnyCondition::Team(cond) => cond.satisfying(self.scores.get_team_scores(), time).map(|eliminated| world.teams_to_players(eliminated)),
		};

		// Avoids the final player score flickering
		if let Some(eliminated) = &eliminated {
			let scores = self.scores.get_player_scores_mut();
			for id in eliminated {
				scores.remove(id);
			}

			if self.controller.remove_empty_teams() {
				let teams = Controller::non_empty_teams(&self.scores, world);
				self.scores.get_team_scores_mut().retain(|id, _| teams.contains(id));
			}
		}

		/*
		 * Need to get the winners after the eliminated players in case the
		 * winning condition is `LastWith` and all players get eliminated at once.
		 * If I updated the winning condition first, the players would be removed
		 * with the game prematurely ending.
		 */
		let winners = match &mut self.win {
			AnyCondition::Player(cond) => cond.satisfying(self.scores.get_player_scores(), time).map(Winners::Players),
			AnyCondition::Team(cond) => cond.satisfying(self.scores.get_team_scores(), time).map(Winners::Teams),
		};

		ScoringUpdateResult { winners, eliminated }
	}

	pub fn init_scores_message(&self) -> Message {
		let mut scores = self.scores.clone();
		if self.hidden.players { scores.get_player_scores_mut().clear(); }
		if self.hidden.teams { scores.get_team_scores_mut().clear(); }
		Message::World(WorldMessage::InitScores(scores))
	}

	pub fn delta_scores_message(&mut self) -> Option<Message> {
		DScores::new(&self.scores, &self.prev_scores).map(|mut delta| {
			delta.apply(&mut self.prev_scores);
			if self.hidden.players { delta.clear_players(); }
			if self.hidden.teams { delta.clear_teams(); }
			Message::World(WorldMessage::DScores(delta))
		})
	}
}
