/*
Copyright (C) 1997-2001 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

#include "g_local.h"
#include "g_gametypes.h"


//==========================================================
//					Teams
//==========================================================

cvar_t	*g_maxteams;
cvar_t	*g_teams_maxplayers;
cvar_t	*g_teams_lock;
cvar_t	*g_teams_teamdamage;

//=================
//G_Teams_NewMap
//=================
void G_Teams_NewMap( void )
{
	int		team;
	edict_t	*e;

	//unlock all teams and clear up team lists
	memset( teamlist, 0, sizeof(teamlist) );
	for( team = TEAM_SPECTATOR; team < GS_MAX_TEAMS; team++ )
		teamlist[team].playerIndices[0] = -1;

	for( e = game.edicts + 1; PLAYERNUM(e) < game.maxclients; e++ ) {
		if( !e->r.client || !e->r.client->pers.connected )
			continue;

		G_Teams_SetTeam( e, TEAM_SPECTATOR );
	}

	level.maptime = 0;
}

//=================
//G_Teams_Init
//=================
void G_Teams_Init( void )
{
	//not much to do here, just start up the cvars
	g_maxteams = trap_Cvar_Get ("g_maxteams", "2", CVAR_ARCHIVE);
	if( g_maxteams->integer > GS_MAX_TEAMS - TEAM_PLAYERS ) {
		trap_Cvar_Set( "g_maxteams", va("%i",(GS_MAX_TEAMS - TEAM_PLAYERS)) );
	}

	g_teams_maxplayers = trap_Cvar_Get( "g_teams_maxplayers", "6", CVAR_ARCHIVE );
	g_teams_lock = trap_Cvar_Get( "g_teams_lock", "0", CVAR_ARCHIVE );
	g_teams_teamdamage = trap_Cvar_Get( "g_teams_teamdamage", "1", CVAR_ARCHIVE );
}


//=================
//G_Teams_UpdateMembersList
// It's better to count the list in detail once per fame, than
// creating a quick list each time we need it.
//=================
void G_Teams_UpdateMembersList( void )
{
	static int			list[MAX_CLIENTS];
	static int			sorted[MAX_CLIENTS];
	static int			count;

	edict_t	*ent;
	int	i, team;
	int	bestscore;
	int	bestplayer = 0;

	for( team = TEAM_SPECTATOR; team < TEAM_RED + g_maxteams->integer; team++ )
	{
		count = 0;
		//create a temp list with the clients inside this team
		for( i = 0, ent = game.edicts + 1; i < game.maxclients; i++, ent++ )
		{
			if( !ent->r.client || !ent->r.client->pers.connected ) continue;
			
			if( ent->s.team == team ) {
				list[count] = ENTNUM(ent);
				count++;
			}
		}

		teamlist[team].numplayers = 0;

		if( count )
		{
			memset( sorted, 0, sizeof(int)*MAX_CLIENTS);
			bestplayer = -2;
			while( bestplayer != -1 )
			{
				bestscore = -9999;
				bestplayer = -1;
				//now sort them by their score
				for( i = 0; i < count; i++ ) {
					if( !sorted[i] )
					{
						ent = game.edicts + list[i];
						if( match.scores[PLAYERNUM(ent)].score >= bestscore ) {
							bestplayer = i;
							bestscore = match.scores[PLAYERNUM(ent)].score;
						}
					}
				}
				
				if( bestplayer > -1 ) {
					sorted[bestplayer] = qtrue;
					teamlist[team].playerIndices[teamlist[team].numplayers] = list[bestplayer];
					teamlist[team].numplayers++;
					
				}
			}
		}

		//terminate the list with -1
		teamlist[team].playerIndices[teamlist[team].numplayers] = -1; 
	}
}

//=================
//G_Teams_TeamIsLocked
//=================
qboolean G_Teams_TeamIsLocked( int team )
{
	if( team && team < GS_MAX_TEAMS )
		return teamlist[team].locked;
	else
		return qfalse;
}

//=================
//G_Teams_LockTeam
//=================
void G_Teams_LockTeam( int team )
{
	if( team && team < GS_MAX_TEAMS )
		teamlist[team].locked = qtrue;
}

//=================
//G_Teams_UnLockTeam
//=================
void G_Teams_UnLockTeam( int team )
{
	if( team && team < GS_MAX_TEAMS )
		teamlist[team].locked = qfalse;
}

//=================
//G_Teams_PlayerIsInvited
//=================
qboolean G_Teams_PlayerIsInvited( int team, edict_t *ent )
{
	int i;

	if( team < TEAM_PLAYERS || team >= GS_MAX_TEAMS )
		return qfalse;

	if( !ent->r.inuse || !ent->r.client )
		return qfalse;

	for( i = 0; teamlist[team].invited[i] && i < MAX_CLIENTS; i++ ) {
		if( teamlist[team].invited[i] == ENTNUM(ent) )
			return qtrue;
	}

	return qfalse;
}

//=================
//G_Teams_InvitePlayer
//=================
void G_Teams_InvitePlayer( int team, edict_t *ent )
{
	int i;

	if( team < TEAM_PLAYERS || team >= GS_MAX_TEAMS )
		return;

	if( !ent->r.inuse || !ent->r.client )
		return;

	for( i = 0; teamlist[team].invited[i] && i < MAX_CLIENTS; i++ ) {
		if( teamlist[team].invited[i] == ENTNUM(ent) )
			return;
	}

	teamlist[team].invited[i] = ENTNUM(ent);
}

//=================
//G_Teams_UnInvitePlayer
//=================
void G_Teams_UnInvitePlayer( int team, edict_t *ent )
{
	int i;

	if( team < TEAM_PLAYERS || team >= GS_MAX_TEAMS )
		return;

	if( !ent->r.inuse || !ent->r.client )
		return;

	for( i = 0; teamlist[team].invited[i] && i < MAX_CLIENTS; i++ ) {
		if( teamlist[team].invited[i] == ENTNUM(ent) )
			break;
	}
	while( teamlist[team].invited[i] && i+1 < MAX_CLIENTS ) {
		teamlist[team].invited[i] = teamlist[team].invited[i+1];
		i++;
	}
	teamlist[team].invited[MAX_CLIENTS-1] = 0;
}

//=================
//G_Teams_RemoveInvites
// Removes all invites from all teams
//=================
void G_Teams_RemoveInvites( void )
{
	int team;

	for( team = TEAM_PLAYERS; team < GS_MAX_TEAMS; team++ )
		teamlist[team].invited[0] = 0;
}

//=================
//G_Teams_Lock_f
//=================
void G_Teams_Lock_f( edict_t *ent )
{
	if( !ent->r.inuse || !ent->r.client || ent->s.team == TEAM_SPECTATOR )
		return;

	if( !g_teams_lock->integer ) {
		G_PrintMsg( ent, "Team locking is not currently enabled on this server.\n" );
		return;
	}

	if( match.state < MATCH_STATE_COUNTDOWN || match.state > MATCH_STATE_PLAYTIME ) {
		G_PrintMsg( ent, "Team locking is only possible during the match.\n" );
		return;
	}

	if( G_Teams_TeamIsLocked( ent->s.team ) ) {
		G_PrintMsg( ent, "Your team is already locked.\n" );
		return;
	}

	G_Teams_LockTeam( ent->s.team );
	G_PrintMsg( ent, "Team %s%s locked by %s%s.\n", GS_TeamName(ent->s.team), S_COLOR_WHITE,
		ent->r.client->pers.netname, S_COLOR_WHITE );
}

//=================
//G_Teams_UnLock_f
//=================
void G_Teams_UnLock_f( edict_t *ent )
{
	if( !ent->r.inuse || !ent->r.client || ent->s.team == TEAM_SPECTATOR )
		return;

	if( !G_Teams_TeamIsLocked( ent->s.team ) ) {
		G_PrintMsg( ent, "Your team is already unlocked.\n" );
		return;
	}

	if( !g_teams_lock->integer ) {
		G_PrintMsg( ent, "Team locking is not currently enabled on this server.\n" );
		return;
	}

	if( match.state < MATCH_STATE_COUNTDOWN || match.state > MATCH_STATE_PLAYTIME ) {
		G_PrintMsg( ent, "Team unlocking is only possible during the match.\n" );
		return;
	}

	G_Teams_UnLockTeam( ent->s.team );
	G_PrintMsg( ent, "Team %s%s unlocked by %s%s.\n", GS_TeamName(ent->s.team), S_COLOR_WHITE,
		ent->r.client->pers.netname, S_COLOR_WHITE );
}

//=================
//G_Teams_Invite_f
//=================
void G_Teams_Invite_f( edict_t *ent )
{
	char *text;
	edict_t *toinvite;

	if( !ent->r.inuse || !ent->r.client || ent->s.team == TEAM_SPECTATOR )
		return;

	text = trap_Cmd_Argv(1);

	if( !text || !strlen(text) ) {
		int i;
		edict_t *e;
		char msg[1024];

		msg[0] = 0;
		Q_strncatz( msg, "Usage: invite <player>\n", sizeof(msg) );
		Q_strncatz( msg, "- List of current players:\n", sizeof(msg) );

		for( i = 0, e = game.edicts+1; i < game.maxclients; i++, e++ ) {
			if( !e->r.inuse ) continue;
			Q_strncatz( msg, va("%3i: %s\n", PLAYERNUM(e), e->r.client->pers.netname), sizeof(msg) );
		}

		G_PrintMsg( ent, "%s", msg );
		return;
	}

	if( !G_Teams_TeamIsLocked( ent->s.team ) ) {
		G_PrintMsg( ent, "Your team is not locked.\n" );
		return;
	}

	toinvite = G_PlayerForText( text );

	if( !toinvite ) {
		G_PrintMsg( ent, "No such player.\n" );
		return;
	}

	if( G_Teams_PlayerIsInvited( ent->s.team, toinvite ) ) {
		G_PrintMsg( ent, "%s%s is already invited to your team.\n", toinvite->r.client->pers.netname, S_COLOR_WHITE );
		return;
	}

	G_Teams_InvitePlayer( ent->s.team, toinvite );
	G_PrintMsg( NULL, "%s%s invited %s%s to team %s%s.\n", ent->r.client->pers.netname, S_COLOR_WHITE,
		toinvite->r.client->pers.netname, S_COLOR_WHITE, GS_TeamName(ent->s.team), S_COLOR_WHITE );
}

//=================
//G_Teams_AssignTeamSkin
//=================
void G_Teams_AssignTeamSkin( edict_t *ent, char *userinfo )
{
	char	skin[MAX_QPATH], model[MAX_QPATH];
	char	*userskin, *usermodel;

	// index skin file
	userskin = GS_TeamSkinName( ent->s.team ); // is it a team skin?
	if( !userskin || !userskin[0] )	// NULL indicates *user defined* 
		userskin = Info_ValueForKey( userinfo, "skin" );
	if( !userskin[0] ) userskin = NULL;

	// index player model
	usermodel = Info_ValueForKey( userinfo, "model" );
	if( !usermodel[0] ) usermodel = NULL;

	if( userskin && usermodel ) {
		Q_snprintfz( model, sizeof(model), "$models/players/%s", usermodel );
		Q_snprintfz( skin, sizeof(skin), "models/players/%s/%s", usermodel, userskin );
	} else {
		Q_snprintfz( model, sizeof(model), "$models/players/%s", DEFAULT_PLAYERMODEL );
		Q_snprintfz( skin, sizeof(skin), "models/players/%s/%s", DEFAULT_PLAYERMODEL, DEFAULT_PLAYERSKIN );
	}

	ent->s.modelindex = trap_ModelIndex( model );
	ent->s.skinnum = trap_SkinIndex( skin );
}

void G_Gametype_CTF_CleanUpPlayerStats( edict_t *ent );
//=================
//G_Teams_SetTeam - sets clients team without any checking
//=================
void G_Teams_SetTeam( edict_t *ent, int team )
{
	//clean scores at changing team
	memset( &match.scores[PLAYERNUM(ent)], 0, sizeof(client_scores_t) );
	if( game.gametype == GAMETYPE_CTF ) {
		G_Gametype_CTF_DeadDropFlag(ent);
		G_Gametype_CTF_CleanUpPlayerStats(ent);
	}
	// remove weapon in the case he had one
	ent->r.client->latched_weapon = 0;
	ChangeWeapon( ent );

	ent->s.team = ent->r.client->pers.team = team;

	if( team == TEAM_SPECTATOR ) {
		client_persistant_t		pers;
		client_teamchange_t		teamchange;
		char	userinfo[MAX_INFO_STRING];
		int		i;

		// reset player ready state
		match.ready[PLAYERNUM(ent)] = qfalse;
		G_AddEvent( ent, EV_TELEPORT, 0, qtrue );

		pers = ent->r.client->pers;
		teamchange = ent->r.client->teamchange;

		memcpy( userinfo, ent->r.client->pers.userinfo, sizeof(userinfo) );
		memset( ent->r.client, 0, sizeof(*ent->r.client) );
		ent->r.client->pers = pers;
		ent->r.client->teamchange = teamchange;
		ClientUserinfoChanged( ent, userinfo );

		// set the delta angle
		for( i = 0; i < 3; i++ )
			ent->r.client->ps.pmove.delta_angles[i] = ANGLE2SHORT(ent->s.angles[i]) - ent->r.client->pers.cmd_angles[i];

		ent->deadflag = DEAD_NO;
		ent->movetype = MOVETYPE_NOCLIP;
		ent->r.solid = SOLID_NOT;
		ent->r.svflags |= SVF_NOCLIENT;

		trap_LinkEntity( ent );

		G_ClearPlayerStateEvents( ent->r.client );

	} else { //known team found

		//G_ClearPlayerStateEvents( ent->r.client );
		
		// call for spawn
		G_Teams_UnInvitePlayer( team, ent );
		G_Gametype_ClientRespawn( ent );		// already updates the skin
	}

	// reset ready up announcer timers
	ent->r.client->pers.readyUpWarningNext = game.realtime + 3000;
	ent->r.client->pers.readyUpWarningCount = 0;

	// reset spawncount etc.
	InitClientResp( ent->r.client );

	G_ScoreboardEvent();
	G_Teams_UpdateMembersList();
	G_Match_CheckReadys();
	G_UpdatePlayerMatchMsg( ent );
}

enum {
	ER_TEAM_OK,
	ER_TEAM_INVALID,
	ER_TEAM_FULL,
	ER_TEAM_LOCKED,
	ER_TEAM_MATCHSTATE,
	ER_TEAM_CHALLENGERS
};

//=================
//G_GameTypes_DenyJoinTeam
//=================
int G_GameTypes_DenyJoinTeam( edict_t *ent, int team )
{
	if( team < 0 || team >= GS_MAX_TEAMS ) {
		G_Printf( "WARNING: 'G_GameTypes_CanJoinTeam' parsing a unrecognized team value\n" );
		return ER_TEAM_INVALID;
	}

	if( team == TEAM_SPECTATOR )
		return ER_TEAM_OK;

	if( match.state > MATCH_STATE_PLAYTIME )
		return ER_TEAM_MATCHSTATE;

	// waiting for chanllengers queue to be executed
	if( G_Gametype_hasChallengersQueue( game.gametype ) &&
		level.maptime < (unsigned)(G_CHALLENGERS_MIN_JOINTEAM_MAPTIME + game.framemsec * 2) )
		return ER_TEAM_CHALLENGERS;

	// force eveyone to go through queue so things work on map change
	if( G_Gametype_hasChallengersQueue( game.gametype ) && !ent->r.client->pers.queueTimeStamp )
		return ER_TEAM_CHALLENGERS;

	//see if team is locked
	if( G_Teams_TeamIsLocked(team) && !G_Teams_PlayerIsInvited(team, ent) ) {
		return ER_TEAM_LOCKED;
	}

	if( GS_Gametype_IsTeamBased(game.gametype) && team >= TEAM_RED && 
		team <= TEAM_YELLOW && (team - TEAM_RED < g_maxteams->integer) ) {
			//see if team is full
			if( team > TEAM_PLAYERS ) 
			{
				int	count = teamlist[team].numplayers;

				if( ( count + 1 > gametypes[game.gametype].maxPlayersPerTeam &&
					gametypes[game.gametype].maxPlayersPerTeam > 0 ) || 
					( count + 1 > g_teams_maxplayers->integer &&
					g_teams_maxplayers->integer > 0 ) ) 
				{
					return ER_TEAM_FULL;
				}
			}

			return ER_TEAM_OK;
		}
	else if( team == TEAM_PLAYERS ) {
		return ER_TEAM_OK;
	}

	return ER_TEAM_INVALID;
}

//=================
//G_Teams_JoinTeam - checks that client can join the given team and then joins it
//=================
qboolean G_Teams_JoinTeam( edict_t *ent, int team )
{
	int error;
	G_Teams_UpdateMembersList(); // make sure we have up-to-date data

	if( !ent->r.client )
		return qfalse;

	if( (error = G_GameTypes_DenyJoinTeam( ent, team )) ) {
		if( error == ER_TEAM_INVALID ) {
			G_PrintMsg( ent, "Can't join %s in %s\n", GS_TeamName( team ),
			GS_Gametype_ShortName(game.gametype) );
		}
		else if( error == ER_TEAM_CHALLENGERS ) {
			G_Teams_JoinChallengersQueue( ent );
		}
		else if( error == ER_TEAM_FULL ) {
			G_PrintMsg( ent, "Team %s is FULL\n", GS_TeamName( team ) );
			G_Teams_JoinChallengersQueue( ent );
		}
		else if( error == ER_TEAM_LOCKED ) {
			G_PrintMsg( ent, "Team %s is LOCKED\n", GS_TeamName( team ) );
			G_Teams_JoinChallengersQueue( ent );
		}
		else if( error == ER_TEAM_MATCHSTATE ) {
			G_PrintMsg( ent, "Can't join %s at this moment\n", GS_TeamName( team ) );
		}
		return qfalse;
	}

	//ok, can join, proceed
	G_Teams_SetTeam( ent, team );
	return qtrue;
}

//=================
//G_Teams_JoinAnyTeam - find us a team since we are too lazy to do ourselves
//=================
qboolean G_Teams_JoinAnyTeam( edict_t *ent, qboolean silent )
{
	int		best = game.maxclients+1;
	int		i, team = -1;
	qboolean	wasinqueue = (ent->r.client->pers.queueTimeStamp);

	G_Teams_UpdateMembersList(); // make sure we have up-to-date data

	//depending on the gametype, of course
	if( !GS_Gametype_IsTeamBased(game.gametype) ) {
		if( ent->s.team == TEAM_PLAYERS ) {
			if( !silent ) {
				G_PrintMsg( ent, "You are already in %s team\n", GS_TeamName(TEAM_PLAYERS) );
			}
			return qfalse;
		}
		if( G_Teams_JoinTeam( ent, TEAM_PLAYERS ) ) {
			if( !silent ) {
				G_PrintMsg( NULL, "%s%s joined the %s team.\n",
					ent->r.client->pers.netname, S_COLOR_WHITE, GS_TeamName(ent->s.team) );
			}
		}
		return qtrue;

	} else {	//team based

		//find the available team with smaller player count
		for( i = TEAM_RED; i < TEAM_RED + g_maxteams->integer; i++ )
		{
			if( G_GameTypes_DenyJoinTeam(ent, i) ) {
					continue;
			}

			if( teamlist[i].numplayers < best ) {
				best = teamlist[i].numplayers;
				team = i;
			}
		}

		if( team == ent->s.team ) { // he is at the right team
			if( !silent ) {
				G_PrintMsg( ent, "%sCouldn't find an emptier team than team %s.\n",
					S_COLOR_WHITE, GS_TeamName(ent->s.team) );
			}
			return qfalse;
		}

		if( team != -1 ) {
			if( G_Teams_JoinTeam( ent, team ) ) {
				if( !silent ) {
					G_PrintMsg( NULL, "%s%s joined the %s team.\n",
						ent->r.client->pers.netname, S_COLOR_WHITE, GS_TeamName(ent->s.team) );
				}
				return qtrue;
			}
		}
		if( match.state <= MATCH_STATE_PLAYTIME && !silent )
			G_Teams_JoinChallengersQueue( ent );
	}

	// don't print message if we joined the queue
	if( !silent && (!G_Gametype_hasChallengersQueue( game.gametype ) || wasinqueue ||
		!ent->r.client->pers.queueTimeStamp) )
		G_PrintMsg( ent, "You can't join the game now\n" );

	return qfalse;
}

//=================
//G_Teams_Join_Cmd
//=================
void G_Teams_Join_Cmd( edict_t *ent )
{
	char	*t;
	int		team;

	t = trap_Cmd_Argv(1);
	if( !t || *t == 0 ) {
		G_Teams_JoinAnyTeam( ent, qfalse );
		return;
	}

	team = GS_Teams_TeamFromName( t );
	if( team != -1 ) {
		if( team == TEAM_SPECTATOR ) { // special handling for spectator team
			Cmd_Spec_f( ent );
			return;
		}
		if( team == ent->s.team ) {
			G_PrintMsg( ent, "You are already in %s team\n", GS_TeamName(team) );
			return;
		}
		if( G_Teams_JoinTeam( ent, team ) ) { //found a team to join
			G_PrintMsg( NULL, "%s%s joined the %s%s team.\n", ent->r.client->pers.netname, S_COLOR_WHITE,
				GS_TeamName(ent->s.team), S_COLOR_WHITE );
			return;
		}
	} else {
		G_PrintMsg( ent, "No such team.\n" );
		return;
	}
}

//======================================================================
//
//TEAM TAB
//
//======================================================================
void LocationName(vec3_t origin, char *buf, int buflen);
int	LocationTAG( char *name );

//==================
//G_Teams_TDM_UpdateTeamTabMessages
//==================
void G_Teams_UpdateTeamTabMessages( void )
{
	static int nexttime = 0;
	static char		teammessage[MAX_STRING_CHARS];
	edict_t			*ent, *e;
	unsigned int	len;
	int				i;
	char			entry[MAX_TOKEN_CHARS];
	char			scratch[MAX_TOKEN_CHARS];
	int				locationTag;


	nexttime -= game.framemsec;
	if( nexttime > 0 )
		return;

	while( nexttime <= 0 ) nexttime += 2 * 1000; // 2 seconds

	// time for a new update
	for( ent = game.edicts + 1; PLAYERNUM(ent) < game.maxclients; ent++ )
	{
		if( !ent->r.inuse || !ent->r.client )
			continue;

		if( ent->s.team <= TEAM_PLAYERS || ent->s.team >= GS_MAX_TEAMS )
			continue;

		*teammessage = 0;
		Q_snprintfz( teammessage, sizeof(teammessage), "ti \"" ); 
		len = strlen(teammessage);

		// add our teammates info to the string
		for( i = 0; teamlist[ent->s.team].playerIndices[i] != -1; i++ )
		{
			e = game.edicts + teamlist[ent->s.team].playerIndices[i];
			if( e == ent )
				continue;

			// get location name
			*scratch = 0;
			LocationName( e->s.origin, scratch, sizeof(scratch) );
			locationTag = LocationTAG( scratch );
			if( locationTag == -1 )
				continue;

			*entry = 0;
			Q_snprintfz( entry, sizeof(entry), "%i %i ",
				PLAYERNUM(e),
				locationTag
				);

			if( MAX_STRING_CHARS - len > strlen(entry) ) {
				Q_strncatz( teammessage, entry, sizeof(teammessage) );
				len = strlen(teammessage);
			}
		}

		// add closing quote
		*entry = 0;
		Q_snprintfz( entry, sizeof(entry), "\"" );
		if( MAX_STRING_CHARS - len > strlen(entry) ) {
			Q_strncatz( teammessage, entry, sizeof(teammessage) );
			len = strlen(teammessage);
		}

		trap_ServerCmd( ent, teammessage );

		// see if there are spectators chasing this player and send them the layout too
		for( i = 0; teamlist[TEAM_SPECTATOR].playerIndices[i] != -1; i++ )
		{
			e = game.edicts + teamlist[TEAM_SPECTATOR].playerIndices[i];
			if( !e->r.inuse || !e->r.client) continue;
			if( e->r.client->chase.active && e->r.client->chase.target == ENTNUM(ent) ) {
				trap_ServerCmd( e, teammessage );
			}
		}
	}
}


//======================================================================
//
// CHALLENGERS QUEUE
//
//======================================================================

//=================
//G_Teams_BestInChallengersQueue
//=================
edict_t *G_Teams_BestInChallengersQueue( unsigned int lastTimeStamp, edict_t *ignore ) {
	edict_t *e, *best = NULL;
	unsigned int bestTimeStamp = game.realtime+10000;

	// fill the teams with the players with the lowest timestamps
	for( e = game.edicts + 1; PLAYERNUM(e) < game.maxclients; e++ ) {
		if( !e->r.inuse || !e->r.client || !e->r.client->pers.connected )
			continue;
		if( !e->r.client->pers.queueTimeStamp || e->s.team != TEAM_SPECTATOR )
			continue;
		if( trap_GetClientState(PLAYERNUM(e)) < CS_SPAWNED || e->r.client->pers.connecting )
			continue;
		if( e == ignore )
			continue;
		if( e->r.client->pers.queueTimeStamp >= lastTimeStamp ) {
			if( e->r.client->pers.queueTimeStamp < bestTimeStamp ) {
				bestTimeStamp = e->r.client->pers.queueTimeStamp;
				best = e;
			}
		}
	}

	return best;
}

//=================
//G_Teams_ExecuteChallengersQueue
//=================
void G_Teams_ExecuteChallengersQueue( void ) {
	edict_t *ent;
	qboolean restartmatch = qfalse;

	// Medar fixme: this is only really makes sense, if playerlimit per team is one
	if( match.state == MATCH_STATE_PLAYTIME )
		return;

	if( !G_Gametype_hasChallengersQueue( game.gametype ) )
		return;

	if( level.maptime < G_CHALLENGERS_MIN_JOINTEAM_MAPTIME ) {
		static int time, lasttime;
		time = (int)((G_CHALLENGERS_MIN_JOINTEAM_MAPTIME - level.maptime)*0.001);
		if( lasttime && time == lasttime )
			return;
		lasttime = time;
		if( lasttime )
			G_CenterPrintMsg( NULL, "Waiting... %i", lasttime );
		else
			G_CenterPrintMsg( NULL, "" );
		return;
	}

	// pick players in join order and try to put them in the
	// game until we get the first refused one.
	ent = G_Teams_BestInChallengersQueue( 0, NULL );
	while( ent && G_Teams_JoinAnyTeam( ent, qtrue ) ) {
		// if we successfully execute the challengers queue during the countdown, revert to warmup
		if( match.state == MATCH_STATE_COUNTDOWN ) {
			restartmatch = qtrue;
		}

		ent = G_Teams_BestInChallengersQueue( ent->r.client->pers.queueTimeStamp, ent );
	}

	if( restartmatch == qtrue ) {
		G_Match_AutorecordCommand( qfalse, qtrue );
		match.state = MATCH_STATE_WARMUP - 1;
		G_Match_SetUpNextState();
	}
}

//=================
////G_Teams_BestScoreBelow
//=================
edict_t *G_Teams_BestScoreBelow( int maxscore ) {
	int team, i;
	edict_t *e, *best = NULL;
	int		bestScore = -9999999;

	if( GS_Gametype_IsTeamBased(game.gametype) ) {
		for( team = TEAM_RED; team < TEAM_RED + g_maxteams->integer; team++ ) {
			for( i = 0; teamlist[team].playerIndices[i] != -1; i++ ) {
				e = game.edicts + teamlist[team].playerIndices[i];
				if( match.scores[PLAYERNUM(e)].score > bestScore &&
					match.scores[PLAYERNUM(e)].score <= maxscore
					&& !e->r.client->pers.queueTimeStamp )
				{
					bestScore = match.scores[PLAYERNUM(e)].score;
					best = e;
				}
			}
		}
	} else {
		for( i = 0; teamlist[TEAM_PLAYERS].playerIndices[i] != -1; i++ ) {
			e = game.edicts + teamlist[TEAM_PLAYERS].playerIndices[i];
			if( match.scores[PLAYERNUM(e)].score > bestScore &&
				match.scores[PLAYERNUM(e)].score <= maxscore
				&& !e->r.client->pers.queueTimeStamp )
			{
				bestScore = match.scores[PLAYERNUM(e)].score;
				best = e;
			}
		}
	}

	return best;
}

//=================
//G_Teams_AdvanceChallengersQueue
//=================
void G_Teams_AdvanceChallengersQueue( void ) 
{
	int i, team, loserscount, winnerscount, playerscount = 0;
	int maxscore = 999999;
	edict_t *won, *e;
	int START_TEAM = TEAM_PLAYERS, END_TEAM = TEAM_PLAYERS+1;

	if( !G_Gametype_hasChallengersQueue( game.gametype ) )
		return;

	G_Teams_UpdateMembersList();

	if( GS_Gametype_IsTeamBased(game.gametype) ) { 
		START_TEAM = TEAM_RED;
		END_TEAM = TEAM_RED + g_maxteams->integer;
	}

	// assign new timestamps to all the players inside teams
	for( team = START_TEAM; team < END_TEAM; team++ ) {
		playerscount += teamlist[team].numplayers;
	}

	if( !playerscount )
		return;

	loserscount = 0;
	if( playerscount > 1 ) {
		loserscount = (int)(playerscount / 2);
	}
	winnerscount = playerscount - loserscount;

	// put everyone who just played out of the challengers queue
	for( team = START_TEAM; team < END_TEAM; team++ ) {
		for( i = 0; teamlist[team].playerIndices[i] != -1; i++ ) {
			e = game.edicts + teamlist[team].playerIndices[i];
			e->r.client->pers.queueTimeStamp = 0;
		}
	}

	// put (back) the best scoring players in first positions of challengers queue
	for( i=0; i < winnerscount; i++ ) {
		won = G_Teams_BestScoreBelow( maxscore );
		if( won ) {
			maxscore = match.scores[PLAYERNUM(won)].score;
			won->r.client->pers.queueTimeStamp = 1 + (winnerscount-i); // never have 2 players with the same timestamp
		}
	}
}

//=================
//G_Teams_LeaveChallengersQueue
//=================
void G_Teams_LeaveChallengersQueue( edict_t *ent ) {
	if( !G_Gametype_hasChallengersQueue( game.gametype ) ) {
		ent->r.client->pers.queueTimeStamp = 0;
		return;
	}

	if( ent->s.team != TEAM_SPECTATOR )
		return;

	// exit the challengers queue
	if( ent->r.client->pers.queueTimeStamp ) {
		ent->r.client->pers.queueTimeStamp = 0;
		G_PrintMsg( ent, "%sYou left the challengers queue\n", S_COLOR_CYAN );
		G_UpdatePlayerMatchMsg( ent );
	}
}

//=================
//G_Teams_JoinChallengersQueue
//=================
void G_Teams_JoinChallengersQueue( edict_t *ent ) {
	int pos = 0;
	edict_t *e;

	if( !G_Gametype_hasChallengersQueue( game.gametype ) ) {
		ent->r.client->pers.queueTimeStamp = 0;
		return;
	}

	if( ent->s.team != TEAM_SPECTATOR )
		return;

	// enter the challengers queue
	if( !ent->r.client->pers.queueTimeStamp ) { // enter the line
		ent->r.client->pers.queueTimeStamp = game.realtime;
		for( e = game.edicts + 1; PLAYERNUM(e) < game.maxclients; e++ ) {
			if( !e->r.inuse || !e->r.client || !e->r.client->pers.connected )
				continue;
			if( !e->r.client->pers.queueTimeStamp || e->s.team != TEAM_SPECTATOR )
				continue;
			// if there are other players with the same timestamp, increase ours
			if( e->r.client->pers.queueTimeStamp >= ent->r.client->pers.queueTimeStamp )
				ent->r.client->pers.queueTimeStamp = e->r.client->pers.queueTimeStamp+1;
			if( e->r.client->pers.queueTimeStamp < ent->r.client->pers.queueTimeStamp )
				pos++;
		}
		G_PrintMsg( ent, "%sYou entered the challengers queue in position %i\n", S_COLOR_CYAN, pos+1 );
		G_UpdatePlayerMatchMsg( ent );
	}
}

//======================================================================
//
//TEAM COMMUNICATIONS
//
//======================================================================

static edict_t *point;
static vec3_t point_location;

//======================================================================
//	LOCATIONS
//======================================================================

void G_RegisterMapLocationName( char *name ) {
	int i;

	if( !name )
		return;

	for( i = 0; i < level.numLocations; i++ ) {
		if( !Q_stricmp( name, level.locationNames[i] ) )
			return;
	}

	Q_strncpyz( level.locationNames[level.numLocations], name, MAX_STRING_CHARS );
	level.numLocations++;
}

int	LocationTAG( char *name )
{
	int i;

	if( !level.numLocations )
		return -1;

	for( i = 0; i < level.numLocations; i++ ) {
		if( !Q_stricmp( name, level.locationNames[i] ) )
			return i;
	}

	return 0;
}

void LocationName(vec3_t origin, char *buf, int buflen)
{
	edict_t *what = NULL;
	edict_t *hot = NULL;
	float hotdist = 3.0f*8192.0f*8192.0f;
	vec3_t v;

	while( (what = G_Find(what, FOFS(classname), "target_location")) != NULL ) {
		VectorSubtract( what->s.origin, origin, v );

		if ( VectorLengthFast( v ) > hotdist ) {
			continue;
		}

		if ( !trap_inPVS ( what->s.origin, origin ) )
			continue;

		hot = what;
		hotdist = VectorLengthFast( v );
	}

	if (!hot || !hot->message)
		Q_snprintfz( buf, buflen, "someplace" );
	else if (hot->count > 0 && hot->count < 10)
		Q_snprintfz( buf, buflen, "%c%c%s", Q_COLOR_ESCAPE, hot->count + '0', hot->message );
	else
		Q_snprintfz( buf, buflen, "%s", hot->message );
}

static void UpdatePoint( edict_t *who )
{
	vec3_t angles, forward, diff;
	trace_t trace;
	edict_t *ent, *ent_best = NULL;
	int i, j;
	float value, value_best = 0.35f; // if nothing better is found, print nothing
	gclient_t *client = who->r.client;
	vec3_t boxpoints[8], viewpoint;

	AngleVectors(client->ps.viewangles, forward, NULL, NULL);
	VectorCopy(who->s.origin, viewpoint);
	viewpoint[2] += who->viewheight;

	for (i = 0; i < game.numentities; i++) {
		ent = game.edicts + i;

		if (!ent->r.inuse || !ent->s.modelindex || ent == who)
			continue;
		if (ent->r.svflags & SVF_NOCLIENT || ent->r.solid == SOLID_NOT)
			continue;
		if (ent->s.type != ET_PLAYER && ent->s.type != ET_ITEM)
			continue;

		VectorSubtract(ent->s.origin, viewpoint, angles);
		VectorNormalize(angles);
		VectorSubtract(forward, angles, diff);
		for (j = 0; j < 3; j++) if (diff[j] < 0) diff[j] = -diff[j];
		value = VectorLengthFast(diff);

		if (value < value_best) {
			BuildBoxPoints( boxpoints, ent->s.origin, ent->r.mins, ent->r.maxs );
			for (j = 0; j < 8; j++) {
				trap_Trace( &trace, viewpoint, vec3_origin, vec3_origin, boxpoints[j], who, MASK_OPAQUE );
				if (trace.fraction == 1) {
					value_best = value;
					ent_best = ent;
					break;
				}
			}
		}
	}

	if (ent_best != NULL) {
		point = ent_best;
		VectorCopy(ent_best->s.origin, point_location);
	} else {
		vec3_t dest;

		VectorMA( viewpoint, 8192, forward, dest );
		trap_Trace( &trace, viewpoint, vec3_origin, vec3_origin, dest, who, MASK_OPAQUE );

		point = NULL;
		VectorCopy(trace.endpos, point_location);
	}
}

static void Say_Team_Location(edict_t *who, char *buf, int buflen, const char *current_color)
{
	LocationName(who->s.origin, buf, buflen);
	Q_strncatz( buf, current_color, buflen );
}

static void Say_Team_Armor(edict_t *who, char *buf, int buflen, const char *current_color)
{
	if (who->r.client->armortag) {
		Q_snprintfz(buf, buflen, "%s%i%s", game.items[who->r.client->armortag]->color,
				ARMOR_TO_INT(who->r.client->armor), current_color);
	} else {
		Q_snprintfz(buf, buflen, "%s0%s", S_COLOR_GREEN, current_color);
	}
}

static void Say_Team_Health(edict_t *who, char *buf, int buflen, const char *current_color)
{
	int health = HEALTH_TO_INT(who->health);

	if (health <= 0)
		Q_snprintfz (buf, buflen, "%s0%s", S_COLOR_RED, current_color);
	else if (health <= 50)
		Q_snprintfz (buf, buflen, "%s%i%s", S_COLOR_YELLOW, health, current_color);
	else if (health <= 100)
		Q_snprintfz (buf, buflen, "%s%i%s", S_COLOR_WHITE, health, current_color);
	else
		Q_snprintfz (buf, buflen, "%s%i%s", S_COLOR_GREEN, health, current_color);
}

static void WeaponString(edict_t *who, int weapon, char *buf, int buflen, const char *current_color)
{
	weapon_info_t *weaponinfo;
	int strong_ammo, weak_ammo;

	Q_snprintfz(buf, buflen, "%s%s%s", (game.items[weapon]->color ? game.items[weapon]->color : ""),
	            game.items[weapon]->short_name, current_color);

	weaponinfo = &g_weaponInfos[weapon];
	strong_ammo = who->r.client->inventory[weaponinfo->firedef->ammo_id];
	weak_ammo = who->r.client->inventory[weaponinfo->firedef_weak->ammo_id];

	if (weapon == WEAP_GUNBLADE)
		Q_strncatz(buf, va(":%i", strong_ammo), buflen);
	else if (strong_ammo > 0)
		Q_strncatz(buf, va(":%i/%i", strong_ammo, weak_ammo), buflen);
	else
		Q_strncatz(buf, va(":%i", weak_ammo), buflen);
}

static qboolean HasItem(edict_t *who, int item)
{
	return (who->r.client->inventory[item]);
}

static void Say_Team_Best_Weapons(edict_t *who, char *buf, int buflen, const char *current_color)
{
	char weapon_strings[2][20];
	int weap, printed = 0;

	for (weap = WEAP_TOTAL-1; weap > WEAP_GUNBLADE; weap--) {
		// evil hack to make RL more important than PG
		if (weap == WEAP_PLASMAGUN) weap = WEAP_ROCKETLAUNCHER;
		else if (weap == WEAP_ROCKETLAUNCHER) weap = WEAP_PLASMAGUN;

		if (HasItem(who, weap)) {
			WeaponString(who, weap, weapon_strings[printed], sizeof(weapon_strings[printed]), current_color);
			if (++printed == 2)
				break;
		}

		if (weap == WEAP_PLASMAGUN) weap = WEAP_ROCKETLAUNCHER;
		else if (weap == WEAP_ROCKETLAUNCHER) weap = WEAP_PLASMAGUN;
	}

	if (printed == 2) {
		Q_snprintfz (buf, buflen, "%s%s %s%s", weapon_strings[1], current_color, weapon_strings[0], current_color);
	} else if (printed == 1) {
		Q_snprintfz (buf, buflen, "%s%s", weapon_strings[0], current_color);
	} else {
		WeaponString(who, WEAP_GUNBLADE, buf, buflen, current_color);
		Q_strncatz(buf, current_color, buflen);
	}
}

static void Say_Team_Current_Weapon(edict_t *who, char *buf, int buflen, const char *current_color)
{
	if (!who->s.weapon) {
		buf[0] = 0;
		return;
	}

	WeaponString(who, who->s.weapon, buf, buflen, current_color);
	Q_strncatz(buf, current_color, buflen);
}

static void Say_Team_Point(edict_t *who, char *buf, int buflen, const char *current_color)
{
	if (!point) {
		Q_snprintfz(buf, buflen, "nothing");
		return;
	}

	if (point->s.type == ET_ITEM) {
		gitem_t *item = GS_FindItemByClassname(point->classname);
		if (item)
			Q_snprintfz(buf, buflen, "%s%s%s", (item->color ? item->color : ""), item->short_name, current_color);
		else
			Q_snprintfz(buf, buflen, point->classname);
	} else {
		Q_snprintfz(buf, buflen, "%s%s", point->classname, current_color);
	}
}

static void Say_Team_Point_Location(edict_t *who, char *buf, int buflen, const char *current_color)
{
	LocationName(point_location, buf, buflen);
	Q_strncatz ( buf, current_color, buflen );
}

static void Say_Team_Pickup(edict_t *who, char *buf, int buflen, const char *current_color)
{
	if (!who->r.client->resp.last_pickup) {
		buf[0] = 0;
	} else {
		gitem_t *item = GS_FindItemByClassname(who->r.client->resp.last_pickup->classname);
		if (item)
			Q_snprintfz(buf, buflen, "%s%s%s", (item->color ? item->color : ""), item->short_name, current_color);
		else
			buf[0] = 0;
	}
}

static void Say_Team_Pickup_Location(edict_t *who, char *buf, int buflen, const char *current_color)
{
	if (!who->r.client->resp.last_pickup) {
		buf[0] = 0;
	} else {
		LocationName(who->r.client->resp.last_pickup->s.origin, buf, buflen);
		Q_strncatz(buf, current_color, buflen);
	}
}

static void Say_Team_Drop(edict_t *who, char *buf, int buflen, const char *current_color)
{
	gitem_t *item = who->r.client->resp.last_drop_item;

	if (!item)
		buf[0] = 0;
	else
		Q_snprintfz(buf, buflen, "%s%s%s", (item->color ? item->color : ""), item->short_name, current_color);
}

static void Say_Team_Drop_Location(edict_t *who, char *buf, int buflen, const char *current_color)
{
	if (!who->r.client->resp.last_drop_item) {
		buf[0] = 0;
	} else {
		LocationName(who->r.client->resp.last_drop_location, buf, buflen);
		Q_strncatz(buf, current_color, buflen);
	}
}

void G_Say_Team(edict_t *who, char *msg, qboolean checkflood)
{
	char outmsg[256];
	char buf[256];
	int i;
	char *p;
	edict_t *cl_ent;
	char current_color[3];

	Q_strncpyz( current_color, S_COLOR_WHITE, sizeof(current_color) );

	if( checkflood ) {
		if(CheckFlood(who))
			return;
	}

	memset(outmsg, 0, sizeof(outmsg));

	if (*msg == '\"') {
		msg[strlen(msg) - 1] = 0;
		msg++;
	}

	UpdatePoint(who);

	for (p = outmsg; *msg && (p - outmsg) < sizeof(outmsg) - 3; msg++) {
		if (*msg == '%') {
			buf[0] = 0;
			switch (*++msg) {
				case 'l':
					Say_Team_Location(who, buf, sizeof(buf), current_color);
					break;
				case 'a':
					Say_Team_Armor(who, buf, sizeof(buf), current_color);
					break;
				case 'h':
					Say_Team_Health(who, buf, sizeof(buf), current_color);
					break;
				case 'b':
					Say_Team_Best_Weapons(who, buf, sizeof(buf), current_color);
					break;
				case 'w':
					Say_Team_Current_Weapon(who, buf, sizeof(buf), current_color);
					break;
				case 'x':
					Say_Team_Point(who, buf, sizeof(buf), current_color);
					break;
				case 'y':
					Say_Team_Point_Location(who, buf, sizeof(buf), current_color);
					break;
				case 'X':
					Say_Team_Pickup(who, buf, sizeof(buf), current_color);
					break;
				case 'Y':
					Say_Team_Pickup_Location(who, buf, sizeof(buf), current_color);
					break;
				case 'd':
					Say_Team_Drop(who, buf, sizeof(buf), current_color);
					break;
				case 'D':
					Say_Team_Drop_Location(who, buf, sizeof(buf), current_color);
					break;
				case '%':
					*p++ = *msg;
					break;
				default :
					// Maybe add a warning here?
					*p++ = '%';
					*p++ = *msg;
			}
			if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 3) {
				Q_strncatz(outmsg, buf, sizeof(outmsg));
				p += strlen(buf);
			}
		} else if (*msg == '^') {
			*p++ = *msg++;
			*p++ = *msg;
			Q_strncpyz( current_color, p-2, sizeof(current_color) );
		} else {
			*p++ = *msg;
		}
	}
	*p = 0;

	for (i = 0; i < game.maxclients; i++) {
		cl_ent = game.edicts + 1 + i;
		if (!cl_ent->r.inuse)
			continue;
		if (cl_ent->s.team == who->s.team)
			G_ChatMsg (cl_ent, "%s[TEAM]%s %s%s: %s\n",
				S_COLOR_YELLOW, S_COLOR_WHITE, who->r.client->pers.netname, S_COLOR_YELLOW, outmsg);
	}
}

