/*
   Copyright (C) 2007 Will Franklin.

   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 "matchmaker.h"
#include "mm_hash.h"
#include "mm_reply.h"

//================
// MMC_Ping
// Simply acknowledges the ping
//================
void MMC_Ping( const socket_t *socket, const netadr_t *address )
{
	// pinger might have sent us args, lets send them back!
	Netchan_OutOfBandPrint( socket, address, "ack %s", Cmd_Args() );
}

//================
// MMC_Acknowledge
// Receives acknowledgement of ping and handles accordingly
//================
void MMC_Acknowledge( const socket_t *socket, const netadr_t *address )
{
	char *type = Cmd_Argv( 1 );

	// gameserver ack
	if( !strcmp( type, "server" ) )
	{
		mm_server_t *server = MM_GetServerFromList( NET_AddressToString( address ) );
		if ( !server )
			return;

		server->lastack = realtime;
		server->noack = 0;
		server->ping = server->lastack - server->lastping;
	}
	// client ack
	else
	{
		mm_match_t *match = matchlist->first;
		mm_client_t **client;
		while( match )
		{
			client = MM_FindClientByAddress( match, address );//MM_FindClientByUserId( match, uid );
			if( client )
			{
				(*client)->lastack = realtime;
				(*client)->noack = 0;
				Com_DPrintf( "Ping acknowledged by %s\n", (*client)->nickname );
			}
			match = match->next;
		}
	}
}

//================
// MMC_Data
// Updates the users data in the database
// Cmd_Argv reference
// 1 = user id
// 2+ = stats in the format:
//      <stattag>=<stat>
//================
void MMC_Data( const socket_t *socket, const netadr_t *address )
{
	char query[MAX_QUERY_SIZE]; // there must be a constant for the max query size?
	char *weap, *arg;
	int uid, dmg, i;

	if( !db_handle )
		Com_Error( ERR_DROP, "MMC_Data: No database connection\n" );

	// playerid must be provided, and must be the first argument
	uid = atoi( Cmd_Argv( 1 ) );
	if( !uid )
		return;

	Q_snprintfz( query, sizeof( query ), "UPDATE `%s` SET ", DBTABLE_USERSTATS );

	for( i = 2; i < Cmd_Argc(); i++ )
	{
		arg = Cmd_Argv( i );
		if( !arg || !*arg )
			continue;

		weap = strtok( arg, "=" );
		dmg = atoi( strtok( NULL, "=" ) );
		if( !dmg || !weap || !*weap )
			continue;

		if( MM_CheckItemExists( weap ) )
			Q_strncatz( query, va( "`%s`=`%s`+'%d',", weap, weap, dmg ), sizeof( query ) );
	}

	// remove trailing comma
	query[strlen( query ) - 1] = '\0';
	Q_strncatz( query, va( " WHERE `id`='%d'", uid ), sizeof( query ) );

	if( DB_Query( db_handle, query ) != DB_SUCCESS )
		Com_Printf( "DB_Query error:\n%s\n", DB_Error( db_handle ) );

}

//================
// MMC_Join
// Flags the user as a matchmaker
// We will update all users
// Cmd_Argv reference
// 1 - user id
// 2 - user nickname
// 3 - ping to mmserver
// 4 - ping type (all pings or around user ping) (defaults to any)
// 4 - gametype (defaults to dm)
// 5 - skill type (all skills or user level dependent) (defaults to any)
//================
void MMC_Join( const socket_t *socket, const netadr_t *address )
{
	int uid, skill_type, gametype, ping, ping_type;
	char *nickname;

	int i, myindex, skill_level = 0;
	mm_match_t *match, *prevmatch;

	char addstr[1024];

	if( !db_handle )
		Com_Error( ERR_DROP, "MMC_Data: No database connection\n" );

	// lets just check all the args are there
	uid = atoi( Cmd_Argv( 1 ) );
#ifndef ALLOW_UNREGISTERED_MM
	if( !uid )
		return;
#endif

	nickname = Cmd_Argv( 2 );
	if( !nickname || !*nickname )
		return;

	ping = atoi( Cmd_Argv( 3 ) );
	// pings cant be negative
	if( ping < 0 )
		return;

	ping_type = atoi( Cmd_Argv( 4 ) );
	if( ping_type < TYPE_ANY || ping_type > TYPE_DEPENDENT )
		return;

	gametype = atoi( Cmd_Argv( 5 ) );
	if( gametype < GAMETYPE_DM || gametype >= GAMETYPE_TOTAL )
		return;

	skill_type = atoi( Cmd_Argv( 6 ) );
	if( skill_type < TYPE_ANY || skill_type > TYPE_DEPENDENT )
		return;

	// unregistered users cant join skill dependent games
	if( skill_type != TYPE_ANY && !uid )
		return;

	// check if the user is a valid user before we add them to our match
	if( !Auth_CheckUserExistsById( uid ) && uid )
		return;

	// do we support this gametype?
	if( !MM_GetGameTypeNameByTag( gametype ) )
		return;

	skill_level = MM_GetSkillLevelByStats( uid );
	if( skill_level == -1 )
		return;

	prevmatch = NULL;
	match = matchlist->first;
	while( match )
	{
		if( MM_MatchesCriteria( match, gametype, skill_type, skill_level, ping_type, ping ) )
			break;

		prevmatch = match;
		match = match->next;
	}

	if( !match )
	{
		match = ( mm_match_t *) MM_Malloc( sizeof( mm_match_t ) );
		match->prev = prevmatch;
		// if prevmatch isnt set, then this is the first match to be created
		if( prevmatch )
			match->prev->next = match;
		else
			matchlist->first = match;

		match->ping_type = ping_type;
		match->gametype = gametype;
		// so for now the maxclients is going to be set by us :)
		MM_GetGameTypeInfo( gametype, &match->maxclients, NULL, NULL, NULL );
		//match->maxclients = MM_GetMaxClientsByTag( gametype );
		match->skill_type = skill_type;
		match->skill_level = ( skill_type == TYPE_DEPENDENT ) ? skill_level : -1;
		match->open = qtrue;
	}


	// unregistered users need a fake user id that the server can use to
	// refer to them. it will be negative to ensure there are no clashes
	if( !uid )
		uid = ++match->fakeuid * -1;

	// we have now found a match (either an existing one or a created one)
	// we now need to find the client a slot
	for( i = 0; i < match->maxclients; i++ )
	{
		if( match->clients[i] )
			continue;

		// allocate space
		match->clients[i] = MM_Malloc( sizeof( mm_client_t ) );
		match->clients[i]->address = MM_Malloc( sizeof( netadr_t ) );
		match->clients[i]->nickname = MM_Malloc( strlen( nickname ) * sizeof( char ) + 1 );
		match->clients[i]->ping = ping;
		match->clients[i]->uid = uid;
		match->clients[i]->skill_level = skill_level;
		memcpy( match->clients[i]->address, address, sizeof( netadr_t ) );
		Q_strncpyz( match->clients[i]->nickname, nickname, strlen( nickname ) * sizeof( char ) + 1 );
		break;
	}
	myindex = i;

	// we need to do 2 things here:
	// - tell the new client about the other clients
	// - tell the other clients about the new client

	// setup the add string for the new client
	Q_strncpyz( addstr, "add", sizeof( addstr ) );
	for( i = 0; i < match->maxclients; i++ )
	{
		if( !match->clients[i] )
			continue;

		Q_strncatz( addstr, va( " %d \"%s\" %d",
		                        match->clients[i]->uid,
		                        match->clients[i]->nickname,
		                        match->clients[i]->skill_level ), sizeof( addstr ) );

		if( match->clients[i]->uid != uid )
			MM_SendMsgToClient( qtrue, match->clients[i], "add %d \"%s\" %d", uid, nickname, skill_level );
	}

	MM_SendMsgToClient( qtrue, match->clients[myindex], addstr );


	// now we want to check if all the client slots have been taken
	// if so, now we need to find a suitable server for the players to play on
	if( MM_ClientCount( match ) == match->maxclients )
		MM_FindAndLockServer( match );
}

//================
// MMC_Drop
// drop client from match and tell other clients about the drop
//================
void MMC_Drop( const socket_t *socket, const netadr_t *address )
{
	mm_match_t *match;
	mm_client_t **client;

	match = matchlist->first;
	while( match )
	{
		client = MM_FindClientByAddress( match, address );
		if( client && *client )
		{
			int uid = ( *client )->uid;
			MM_FreeClient( *client );
			*client = NULL;
			if( MM_ClientCount( match ) )
				MM_SendMsgToClients( qtrue, match, "drop %d", uid );
			else
				MM_FreeMatch( match );

			break;
		}
		match = match->next;
	}
}

//================
// MMC_Reply
// Gameserver is replying to our cmd request
//================
void MMC_Reply( const socket_t *socket, const netadr_t *address )
{
	char *reply;
	mm_server_t *server = MM_GetServerFromList( NET_AddressToString( address ) );

	if( !server )
		return;

	// why is server replying, we didnt send anything
	if( !server->cmd.lastcmd[0] )
		return;

	if( Cmd_Argc() != 2 )
		return;

	reply = Cmd_Argv( 1 );
	if( !reply || !*reply )
		return;

	// generic reply that doesn't need any specific attention
	if( !Q_stricmp( reply, "nocmd" ) )
	{
		MM_SendCmdToGameServer( server, server->cmd.lastcmd );
		return;
	}

	// clear last cmd, as it was received correctly
	memset( &server->cmd, sizeof( server->cmd ), 0 );

	// call reply callback function for this server
	if( server->cmd.callback )
		server->cmd.callback( ( void* )server, reply );
}

//================
// MM_PingServer
// A client has replied to our gameserver ping request
//================
#ifdef OLD_GAMESERVER_CHECKING
void MMC_PingServer( const socket_t *socket, const netadr_t *address )
{
	int ping, i, maxping = 0, minping = 0, count = 0;
	mm_server_t *server;
	mm_match_t *match;
	mm_client_t **client;

	Com_Printf( "Ping Server received\n" );
	if( Cmd_Argc() != 3 )
		return;

	server = MM_GetServerFromList( Cmd_Argv( 1 ) );
	if( !server )
		return;

	match = server->match;
	if( !match )
		return;

	client = MM_FindClientByAddress( match, address );
	if( !client )
		return;

	if( !Q_stricmp( Cmd_Argv( 2 ), "noreply" ) )
		ping = MAX_PING + 1; // guarenteeded to fail
	else
		ping = atoi( Cmd_Argv( 2 ) );

	Com_Printf( "looking good\n" );
	if( ping > MAX_PING )
	{
		MM_SendMsgToClients( match, "%s's ping is too high", (*client)->nickname );

		MM_AddToIgnoreList( match, server->ip );
		MM_ClearServer( server );
		MM_FindAndLockServer( match );
		return;
	}

	(*client)->serverping = ping;

	for( i = 0; i < match->maxclients; i++ )
	{
		if( !match->clients[i] )
			continue;

		if( !match->clients[i]->serverping )
			continue;

		count++;
		maxping = max( maxping, match->clients[i]->serverping );
		minping = min( minping, match->clients[i]->serverping );
	}

	if( maxping - minping > MAX_PING_DEVIATION && count > 1 )
	{
		MM_SendMsgToClients( match, "ping deviation is too high" );

		MM_AddToIgnoreList( match, server->ip );
		MM_ClearServer( server );
		MM_FindAndLockServer( match );
		return;
	}

	// all clients have replied
	if( count == match->maxclients )
	{
		// send them the start message.
		server->cmd.callback = MM_ReplyToUnlock;
		MM_SendCmdToGameServer( server, "unlock" );
		MM_SendMsgToClients( match, "status preparing server for match" );
	}
}
#endif

//================
// MMC_Heartbeat
// Deals with the heartbeat sent by a gameserver
//================
void MMC_Heartbeat( const socket_t *socket, const netadr_t *address )
{
	char *mm, *ip;
	mm_server_t *server;

	if ( Cmd_Argc() != 2 )
		return;

	mm = Cmd_Argv( 1 );
	if ( !mm || !*mm )
		return;

	ip = NET_AddressToString( address );
	server = MM_GetServerFromList( ip );
	if ( !strcmp( mm, "yes" ) )
	{
		if ( !server )
			server = MM_AddServerToList( ip );

		//server->lastping = realtime;
	}
	// delete server if it exists.
	else if ( server )
		MM_DeleteServerFromList( server->ip );

	Com_DPrintf( "Received heartbeat from %s: %s\n", ip, mm );
}

//================
// MMC_Chat
// relays a chat message to clients
//================
void MMC_Chat( const socket_t *socket, const netadr_t *address )
{
	mm_match_t *match;
	mm_client_t **client = NULL;
	int type = atoi( Cmd_Argv( 1 ) );
	char *msg = Cmd_Argv( 2 );

	if( !msg || !*msg )
		return;

	for( match = matchlist->first ; match ; match = match->next )
	{
		if( ( client = MM_FindClientByAddress( match, address ) ) )
			break;
	}

	if( !match || !client )
		return;

	// broadcast this message just to the people in this player's match
	if( type )
	{
		MM_SendMsgToClients( qtrue, match, "chat \"%s%s:\" \"%s%s\"", S_COLOR_YELLOW, (*client)->nickname, S_COLOR_YELLOW, msg );
		return;
	}

	// broadcast this message to everyone in a match
	for( match = matchlist->first ; match ; match = match->next )
		MM_SendMsgToClients( qtrue, match, "chat \"%s%s:\" \"%s%s\"", S_COLOR_WHITE, (*client)->nickname, S_COLOR_GREEN, msg );

}

//================
// MMC_AcknowledgePacket
// Clears a packet from a clients packet list
//================
void MMC_AcknowledgePacket( const socket_t *socket, const netadr_t *address )
{
	mm_match_t *match;
	mm_client_t **client;
	mm_packet_t *packet, *prev = NULL, *next;
	char *hash = Cmd_Argv( 1 );

	if( Cmd_Argc() != 2 )
		return;

	if( !hash || !*hash )
		return;

	for( match = matchlist->first ; match ; match = match->next )
	{
		client = MM_FindClientByAddress( match, address );
		if( !client )
			continue;

		// check for cmd in packet list
		for( packet = (*client)->packets ; packet ; )
		{
			if( !strcmp( packet->hash, hash ) )
			{
				prev = packet;
				continue;
			}
			
			Com_DPrintf( "Packet acknowledged: %s\n", packet->cmd );
			// delete from packet list
			next = packet->next;
			MM_Free( packet->cmd );
			MM_Free( packet );
			packet = next;
			if( prev )
				prev->next = next;
			else
				(*client)->packets = next;
		}
	}
}

//================
// MM_FindAndLockServer
// Finds a server and locks it, ready for match
//================
void MM_FindAndLockServer( mm_match_t *match )
{
	mm_server_t *server;
	char randno[5], hash[HASH_SIZE];

	// we should only be connecting to a server when we have all clients
	if( MM_ClientCount( match ) != match->maxclients )
		return;

	server = MM_GetSuitableServer( match );
	if( server )
	{
		MM_SendMsgToClients( qfalse, match, "status acquiring server lock" );
		server->match = match;
		server->cmd.callback = MM_ReplyToSetup;
		// generate a random pass
		Q_snprintfz( randno, sizeof( randno ), "%u", ( unsigned int )brandom( 0, pow( 10, sizeof( randno ) - 1 ) ) );
		MM_Hash( hash, randno );
		Q_strncpyz( server->pass, hash, sizeof( server->pass ) );
		// send cmd
		MM_SendCmdToGameServer( server, va( "setup %s %d", server->pass, match->gametype ) );
		match->awaitingserver = 0;
	}
	else
	{
		// only resend status if this is the first time we are trying
		// to find a server
		if( !match->awaitingserver )
			MM_SendMsgToClients( qfalse, match, "status awaiting suitable server" );
		match->awaitingserver = realtime;
	}
}
