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

use crate::world::{World, player::{PlayerId, PlayerStyle, HumanStyle, config::PlayerConfig}, team::TeamId};

#[derive(Default)]
pub struct Joined(BTreeMap<PlayerId, JoinState>);

#[derive(Clone)]
pub struct JoinInfo<S> {
	pub style: S,
	pub team: Option<TeamId>,
	pub config: Option<PlayerConfig>,
}

impl JoinInfo<HumanStyle> {
	pub fn update_team(&mut self, team_id: TeamId, world: &World) {
		let Some(team) = world.get_team(team_id) else { return; };
		self.team = Some(team_id);
		self.style.colour = team.get_colour();
	}
}

impl From<JoinInfo<HumanStyle>> for JoinInfo<PlayerStyle> {
	fn from(info: JoinInfo<HumanStyle>) -> JoinInfo<PlayerStyle> {
		JoinInfo {
			style: PlayerStyle::Human(info.style),
			team: info.team,
			config: info.config,
		}
	}
}

enum JoinState {
	NewPlaying(JoinInfo<PlayerStyle>), // A new player has joined playing
	OldPlaying(JoinInfo<PlayerStyle>), // A previously spectating player has started playing
	NewSpectating, // A new player has joined spectating
}

impl Joined {
	pub(super) fn add_client_spectating(&mut self, id: PlayerId) {
		let prev = self.0.insert(id, JoinState::NewSpectating);
		debug_assert!(prev.is_none());
	}

	pub(super) fn add_client_playing(&mut self, id: PlayerId, info: JoinInfo<PlayerStyle>) {
		let prev = self.0.insert(id, JoinState::NewPlaying(info));
		debug_assert!(prev.is_none());
	}

	pub(super) fn add_player(&mut self, id: PlayerId, info: JoinInfo<PlayerStyle>) {
		match self.0.entry(id) {
			Entry::Occupied(mut e) => {
				match e.get() {
					// Don't want to forget that this is a new client, or the initial messages won't be sent out
					JoinState::NewPlaying(..) | JoinState::OldPlaying(..) => (),
					JoinState::NewSpectating => _ = e.insert(JoinState::NewPlaying(info)),
				}
			},
			Entry::Vacant(e) => _ = e.insert(JoinState::OldPlaying(info)),
		}
	}

	pub(super) fn is_new_client(&self, id: PlayerId) -> bool {
		match self.0.get(&id) {
			Some(JoinState::NewPlaying(..) | JoinState::NewSpectating) => true,
			Some(JoinState::OldPlaying(..)) | None => false,
		}
	}

	pub fn players(&self) -> impl Iterator<Item = (PlayerId, JoinInfo<PlayerStyle>)> + '_ {
		self.0
			.iter()
			.filter_map(|(&id, state)| match state {
				JoinState::NewPlaying(info) | JoinState::OldPlaying(info) => Some((id, info.clone())),
				JoinState::NewSpectating => None,
			})
	}
}
