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

use super::{ScoringEvent, Scores, BTreeMap, BTreeSet};
use super::{HiddenScores, super::config::{ScoringControllerConfig, ScoringControllerSubconfig, ScoringEventListener, OccupationZone as OccupationZoneConfig}};

use crate::world::{World, player::{PlayerId, RADIUS}, team::{Team, TeamId}, special_area::SpecialArea};
use crate::utils::{Clock, maths::CollidingRect};

pub struct Controller {
	player_subcon: SubController,
	team_subcon: SubController,

	// Not putting in subcontroller as this only applies to team scores
	all_flags_dscore: i32,
	flag_dscores: BTreeMap<usize, i32>,

	remove_empty_teams: bool,
}

struct SubController {
	config: ScoringControllerSubconfig,

	kill: i32,
	death: i32,
	clocks: Vec<(Clock, i32)>,
	occupation_zones: Vec<OccupationZone>,
}

impl SubController {
	pub fn new(subconfig: ScoringControllerSubconfig) -> SubController {
		SubController {
			config: subconfig,
			kill: 0, death: 0,
			clocks: Vec::new(), occupation_zones: Vec::new(),
		}
	}

	pub fn reset(&mut self) {
		for (clock, _) in &mut self.clocks {
			clock.set_next_update(clock.get_interval());
		}

		for zone in &mut self.occupation_zones {
			zone.colliding.clear();
		}
	}

	pub fn change_players<I, O, N>(&self, scores: &mut BTreeMap<I, i32>, mut has_old: O, new_iter: N)
	where
		I: Copy + Ord,
		O: FnMut(I) -> bool,
		N: Iterator<Item = I>,
	{
		// Removes scores from old players/teams (not needed for now in the teams case but might be useful in the future)
		scores.retain(|&id, _| has_old(id));

		// Adds scores from new players/teams
		for id in new_iter {
			scores.entry(id).or_insert(self.config.init);
		}
	}

	pub fn get_clock_dscore(&mut self, dt: f32) -> i32 {
		let mut dscore = 0;
		for (clock, ds) in &mut self.clocks {
			let steps = clock.advance(dt);
			dscore += *ds * steps as i32;
		}
		dscore
	}

	pub fn get_occupation_zone_dscore(&mut self, player_id: PlayerId, team_id: Option<TeamId>, pos: Vec2, dt: f32) -> i32 {
		let mut dscore = 0;
		for zone in &mut self.occupation_zones {
			// Can't capture your own occupation zone
			if team_id.is_some() && team_id == zone.config.owner { continue; }

			let colliding = (pos, RADIUS).colliding_rect(&zone.config.rect);

			if colliding {
				let clock = zone.colliding.entry(player_id).or_insert_with(|| Clock::with_next_update(zone.config.period, zone.config.capture_time));
				let steps = clock.advance(dt);
				dscore += steps as i32 * zone.dscore;
			} else {
				zone.colliding.remove(&player_id);
			}
		}
		dscore
	}

	fn clamp(&self, mut score: i32) -> i32 {
		if let Some(min) = self.config.min { score = score.max(min); }
		if let Some(max) = self.config.max { score = score.min(max); }
		score
	}

	fn change(&self, score: &mut i32, dscore: i32) {
		*score = self.clamp(score.saturating_add(dscore));
	}
}

struct OccupationZone {
	config: OccupationZoneConfig,
	dscore: i32,
	colliding: BTreeMap<PlayerId, Clock>,
}

impl Controller {
	pub fn new(config: ScoringControllerConfig, teams: &[Team], special_areas: &mut Vec<SpecialArea>) -> (Controller, HiddenScores) {
		let (mut player_subcon, mut team_subcon) = (SubController::new(config.player), SubController::new(config.team));

		let mut all_flags_dscore = 0;
		let mut flag_dscores = BTreeMap::new();

		let mut hidden = HiddenScores { players: true, teams: true };

		for event in config.events {
			let controllers = [(&mut player_subcon, event.output.player()), (&mut team_subcon, event.output.team())]
				.into_iter()
				.filter_map(|(subcon, add)| add.then_some(subcon));

			if event.output.player() && !matches!(event.listener, ScoringEventListener::Flag(_)) /* Doesn't change player scores */ { hidden.players = false; }
			if event.output.team() { hidden.teams = false; }

			for subcon in controllers {
				match event.listener {
					ScoringEventListener::Kill => subcon.kill += event.score,
					ScoringEventListener::Death => subcon.death += event.score,
					ScoringEventListener::Time(time) => subcon.clocks.push((Clock::new(time), event.score)),
					ScoringEventListener::Occupation(ref config, ref colour) => {
						if let Some(colour) = colour {
							let team_colour = config.owner.and_then(|id| teams.get(id.index())).map(Team::get_colour);
							special_areas.extend_from_slice(&SpecialArea::rect(config.rect, colour.get(team_colour)));
						}

						subcon.occupation_zones.push(OccupationZone {
							config: config.clone(),
							dscore: event.score,
							colliding: BTreeMap::new(),
						});
					},
					ScoringEventListener::Flag(_) => (),
				}
			}

			if event.output.team() && let ScoringEventListener::Flag(index) = event.listener {
				if let Some(index) = index {
					*flag_dscores.entry(index).or_default() += event.score;
				} else {
					all_flags_dscore += event.score;
				}
			}
		}

		(Controller { player_subcon, team_subcon, all_flags_dscore, flag_dscores, remove_empty_teams: config.remove_empty_teams }, hidden)
	}

	pub fn reset(&mut self) {
		self.player_subcon.reset();
		self.team_subcon.reset();
	}

	pub fn remove_empty_teams(&self) -> bool {
		self.remove_empty_teams
	}

	pub fn non_empty_teams(scores: &Scores, world: &World) -> BTreeSet<TeamId> {
		scores
			.get_player_scores()
			.keys()
			.filter_map(|&id| world.get_player_team(id))
			.collect()
	}

	pub fn change_players(&self, scores: &mut Scores, world: &World) {
		self.player_subcon.change_players(scores.get_player_scores_mut(), |id| world.has_player(id), world.player_id_iter());

		if self.remove_empty_teams {
			let teams = Controller::non_empty_teams(scores, world);
			self.team_subcon.change_players(scores.get_team_scores_mut(), |id| teams.contains(&id), teams.iter().copied());
		} else {
			self.team_subcon.change_players(scores.get_team_scores_mut(), |id| world.has_team(id), world.team_id_iter());
		}
	}

	pub fn event(&mut self, event: ScoringEvent, scores: &mut Scores) {
		match event {
			ScoringEvent::Kill(player_id, team_id) => self.change(player_id, team_id, self.player_subcon.kill, self.team_subcon.kill, scores),
			ScoringEvent::Death(player_id, team_id) => self.change(player_id, team_id, self.player_subcon.death, self.team_subcon.death, scores),
			ScoringEvent::Time(dt) => {
				let (player, team) = (self.player_subcon.get_clock_dscore(dt), self.team_subcon.get_clock_dscore(dt));
				self.change_all(player, team, scores);
			},
			ScoringEvent::Pos(player_id, team_id, pos, dt) => {
				let (player, team) = (self.player_subcon.get_occupation_zone_dscore(player_id, team_id, pos, dt), self.team_subcon.get_occupation_zone_dscore(player_id, team_id, pos, dt));
				self.change(player_id, team_id, player, team, scores);
			},
			ScoringEvent::Flag(team_id, index) => {
				let dscore = self.all_flags_dscore + self.flag_dscores.get(&index).copied().unwrap_or_default();
				self.change_team(team_id, dscore, scores);
			},
		}
	}

	fn change(&self, player_id: PlayerId, team_id: Option<TeamId>, player_score: i32, team_score: i32, scores: &mut Scores) {
		scores.get_player_scores_mut().entry(player_id).and_modify(|s| self.player_subcon.change(s, player_score));
		if let Some(id) = team_id {
			self.change_team(id, team_score, scores);
		}
	}

	fn change_team(&self, id: TeamId, score: i32, scores: &mut Scores) {
		scores.get_team_scores_mut().entry(id).and_modify(|s| self.team_subcon.change(s, score));
	}

	fn change_all(&self, player_score: i32, team_score: i32, scores: &mut Scores) {
		for s in scores.get_player_scores_mut().values_mut() { self.player_subcon.change(s, player_score); }
		for s in scores.get_team_scores_mut().values_mut() { self.team_subcon.change(s, team_score); }
	}
}
