/*
   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 "client.h"
#include "../matchmaker/mm_hash.h"

// Matchmaker shouldn't really reuse our main socket, so disable TCP support for now
#undef TCP_SUPPORT

static qboolean mm_initialized = qfalse;
mempool_t *clmm_mempool;

cvar_t *cl_mmserver;

// we keep a copy of the match details with the client
static mm_match_t *mm_match;
// this can differ from the myuserid variable if the user isnt logged in
// static int matchuserid;

// for joining a match
static int igametype;
static mm_type_t iskill, iping;
static unsigned int mmpinged;

#ifdef OLD_GAMESERVER_CHECKING
#define MM_SERVER_PING_TIMEOUT 5000
typedef struct mm_serverping_s
{
	unsigned int pinged;
	char ip[32];
	struct mm_serverping_s *next;

} mm_serverping_t;

static mm_serverping_t *serverpings;
#endif

static mm_packet_t *clmm_packets;
static int clmm_packetno;

static void CL_MM_FreePackets( void )
{
	mm_packet_t *packet = clmm_packets, *next;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	while( packet )
	{
		next = packet->next;
		MM_Free( packet->cmd );
		MM_Free( packet );
		packet = next;
	}
	clmm_packets = NULL;
	clmm_packetno = 0;
}

//================
// CL_MM_SendMsgToServer
// Sends a msg to mm and adds it to the packet list
//================
static void CL_MM_SendMsgToServer( qboolean reliable, const char *format, ... )
{
	va_list argptr;
	char cmd[1024], hash[HASH_SIZE + 1], *pno;
	mm_packet_t *packet;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	va_start( argptr, format );
	Q_vsnprintfz( cmd, sizeof( cmd ), format, argptr );
	va_end( argptr );

	if( !*cmd )
		return;

	// we need append a packet number
	if( reliable && !strchr( cmd, PACKET_NO_DELIM ) )
		Q_strncatz( cmd, va( "%c%d", PACKET_NO_DELIM, ++clmm_packetno ), sizeof( cmd ) );

	Com_DPrintf( "Sending to matchmaker: %s\n", cmd );
	MM_SendMsgToServer( &cls.socket_udp, cmd );

	if( !reliable )
		return;

	pno = strchr( cmd, PACKET_NO_DELIM );
	*pno = 0;
	// hash cant have packet number attached
	MM_Hash( hash, cmd );
	*pno = PACKET_NO_DELIM;

	// if packet already exists, just update senttime
	for( packet = clmm_packets ; packet ; packet = packet->next )
	{
		if( !strcmp( packet->hash, hash ) )
		{
			packet->senttime = cls.realtime;
			break;
		}
	}

	if( packet )
		return;

	// add msg to packet list
	packet = MM_Malloc( sizeof( mm_packet_t ) );
	packet->cmd = MM_Malloc( strlen( cmd ) + 1 );
	strcpy( packet->cmd, cmd );
	strcpy( packet->hash, hash );
	packet->senttime = cls.realtime;
	packet->next = clmm_packets;
	clmm_packets = packet;
}

//================
// CL_MM_CheckPackets
// Resend any packets the matchmaker doesnt seem to have received
//================
void CL_MM_CheckPackets( void )
{
	mm_packet_t *packet;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	for( packet = clmm_packets ; packet ; packet = packet->next )
	{
		// packet doesnt appear to have been received, lets resend
		if( packet->senttime + PACKET_PING_TIME < cls.realtime )
			CL_MM_SendMsgToServer( qtrue, packet->cmd );
	}
}

//================
// CL_MM_Init
// Initialize matchmaking components
//================
void CL_MM_Init( void )
{
#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( mm_initialized )
		return;

	mm_mempool = Mem_AllocPool( NULL, "Matchmaker" );

	mm_match = NULL;
	clmm_packets = NULL;

	cl_mmserver = Cvar_Get( MM_SERVER_VAR, MM_SERVER_IP, CVAR_ARCHIVE );

	mm_initialized = qtrue;
}

//================
// CL_MM_Shutdown
// Shutdown matchmaking stuff
//================
void CL_MM_Shutdown( void )
{
#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( !mm_initialized )
		return;

	if( mm_match )
		CL_MM_Drop();

	CL_MM_FreePackets();

	Mem_FreePool( &mm_mempool );

	mm_initialized = qfalse;
}

#ifdef OLD_GAMESERVER_CHECKING
//================
// CL_MM_Frame
// Delete servers that havent responded to our ping within a certain time
//================
void CL_MM_Frame( void )
{
	mm_serverping_t *server, *prev = NULL;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	for( server = serverpings; server; server = server->next )
	{
		if( server->pinged + MM_SERVER_PING_TIMEOUT < cls.realtime )
			break;

		prev = server;
	}

	// no servers have timed out
	if( !server )
		return;

	// delete from list
	CL_MM_SendMsgToServer( "pingserver %s noreply", server->ip );

	if( prev )
		prev->next = server->next;
	else
		serverpings = server->next;

	MM_Free( server );
}
#endif

//================
// CL_MMC_Add
// A player has joined the matchmaking and we ned to update our local copy
// to display in the UI
//================
static void CL_MMC_Add( const socket_t *socket, const netadr_t *address )
{
	char *nickname;
	int userid, skill_level;

	int i, j;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( !mm_match )
		return;

	// -1 because of the packet name, %3 because the number of args must be a multiple of 3
	if( ( Cmd_Argc() - 1 ) % 3 )
		return;

	for( i = 1; i < Cmd_Argc(); i += 3 )
	{
		userid = atoi( Cmd_Argv( i ) );
		nickname = Cmd_Argv( i + 1 );
		skill_level = atoi( Cmd_Argv( i + 2 ) );

		if( !userid || !nickname || !*nickname )
			continue;

		for( j = 0; j < mm_match->maxclients; j++ )
		{
			if( mm_match->clients[j] )
				continue;

			mm_match->clients[j] = MM_Malloc( sizeof( mm_client_t ) );
			mm_match->clients[j]->nickname = MM_Malloc( strlen( nickname ) * sizeof( char ) + 1 );
			mm_match->clients[j]->uid = userid;
			Q_strncpyz( mm_match->clients[j]->nickname, nickname, strlen( nickname ) * sizeof( char ) + 1 );
			mm_match->clients[j]->skill_level = skill_level;
			mm_match->clients[j]->address = NULL;

			// tell the UI about the new player
			CL_UIModule_MM_UpdateSlot( j, mm_match->clients[j]->nickname );
			break;
		}
	}
	CL_UIModule_MM_UpdateStatus( "waiting for players" );
}

//================
// CL_MMC_Drop
// A player has dropped from the match and we need to update our local copy
// to display in the UI
//================
static void CL_MMC_Drop( const socket_t *socket, const netadr_t *address )
{
	int i, userid = atoi( Cmd_Argv( 1 ) );

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( !userid )
		return;

	if( !mm_match )
		return;

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

		if( mm_match->clients[i]->uid == userid )
		{
			CL_UIModule_MM_UpdateSlot( i, va( "Slot %d", i + 1 ) );

			MM_FreeClient( mm_match->clients[i] );
			mm_match->clients[i] = NULL;
		}
	}
}

//================
// CL_MMC_Start
// Match is full and its time to connect
//================
static void CL_MMC_Start( const socket_t *socket, const netadr_t *address )
{
	char *ip = Cmd_Argv( 1 ),
		*pass = Cmd_Argv( 2 );

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( !mm_match )
		return;

	if( !ip || !*ip || !pass || !*pass )
		return;

	MM_FreeMatch( mm_match );
	mm_match = NULL;

	CL_MM_FreePackets();

	// rather crude, no checking is done
	CL_UIModule_MM_UpdateStatus( "match is ready, connecting..." );
	Cbuf_ExecuteText( EXEC_APPEND, va( "wait; connect %s; password %s\n", ip, pass ) );
}

//================
// CL_MMC_Shutdown
// Matchmaking server is shutting down
//================
static void CL_MMC_Shutdown( const socket_t *socket, const netadr_t *address )
{
	int i;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( !mm_match )
		return;

	for( i = 0; i < mm_match->maxclients; i++ )
		CL_UIModule_MM_UpdateSlot( i, va( "Slot %d", i + 1 ) );
	CL_UIModule_MM_UpdateStatus( "matchmaker shutdown" );
	CL_UIModule_MM_UpdateMatchStatus( qfalse );
	
	CL_MM_FreePackets();

	MM_FreeMatch( mm_match );
	mm_match = NULL;
}

//================
// CL_MMC_PingServer
// Ping the gameserver that the matchmaker has asked us to ping
//================
#ifdef OLD_GAMESERVER_CHECKING
static void CL_MMC_PingServer( const socket_t *socket, const netadr_t *address )
{
	char *ip;
	netadr_t adr;
	mm_serverping_t *server;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( Cmd_Argc() != 2 )
		return;

	ip = Cmd_Argv( 1 );

	// empty
	if( !ip || !*ip )
		return;

	// bad ip?
	if( !NET_StringToAddress( ip, &adr ) )
		return;

	// bad ip?
	if( adr.type != NA_IP )
		return;

	if( !adr.port )
		adr.port = BigShort( PORT_SERVER );

	// send ping
	Netchan_OutOfBandPrint( &cls.socket_udp, &adr, "ping mm" );

	// add to serverpings list
	server = MM_Malloc( sizeof( mm_serverping_t ) );
	Q_strncpyz( server->ip, ip, sizeof( server->ip ) );
	server->pinged = cls.realtime;
	server->next = serverpings;
	serverpings = server;
}
#endif

//================
// CL_MMC_Status
// Update UI with status message from matchmaker
//================
static void CL_MMC_Status( const socket_t *socket, const netadr_t *address )
{
#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( !mm_match )
		return;

	if( Cmd_Argc() < 2 )
		return;

	CL_UIModule_MM_UpdateStatus( Cmd_Args() );
}

//================
// CL_MMC_Ack
//================
static void CL_MMC_Ack( const socket_t *socket, const netadr_t *address )
{
#ifdef OLD_GAMESERVER_CHECKING
	char ip[32];
	mm_serverping_t *server, *prev = NULL;
#endif
	char *type = Cmd_Argv( 1 );

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	// we pinged the mm server, now we received our reply lets send a join request
	if( !strcmp( type, "join" ) )
	{
		// we didnt ping mm
		if( !mmpinged )
			return;

		// setup our local match copy
		if( mm_match )
			CL_MM_Drop();

		mm_match = ( mm_match_t * ) MM_Malloc( sizeof( mm_match_t ) );
		memset( mm_match, 0, sizeof( mm_match_t ) );

		MM_GetGameTypeInfo( igametype, &mm_match->maxclients, NULL, NULL, NULL );

		CL_MM_SendMsgToServer( qtrue, "join %d \"%s\" %d %d %d %d", 0, Cvar_String( "name" ), cls.realtime - mmpinged, iping, igametype, iskill );
		CL_UIModule_MM_UpdateStatus( "connecting to matchmaker" );
		CL_UIModule_MM_UpdateMatchStatus( qtrue );
		mmpinged = 0;
		return;
	}

#ifdef OLD_GAMESERVER_CHECKING
	// we pinged a gameserver, now they have replied
	if( !strcmp( type, "mm" ) )
	{
		if( address->type != NA_IP )
			return;

		Q_strncpyz( ip, NET_AddressToString( address ), sizeof( ip ) );
		if( !ip[0] )
			return;

		for( server = serverpings; server; server = server->next )
		{
			if( !Q_stricmp( server->ip, ip ) )
				break;

			prev = server;
		}

		// server not in list
		if( !server )
			return;

		// tell matchmaker our ping
		CL_MM_SendMsgToServer( "pingserver %s %i", ip, cls.realtime - server->pinged );

		// remove from list
		if( prev )
			prev->next = server->next;
		else
			serverpings = server->next;

		MM_Free( server );
	}
#endif
}

//================
// CL_MMC_Chat
// Received a chat message from the mmserver
//================
static void CL_MMC_Chat( const socket_t *socket, const netadr_t *address )
{
	char *nick, *msg;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( !mm_match )
		return;

	if( Cmd_Argc() != 3 )
		return;

	nick = Cmd_Argv( 1 );
	msg = Cmd_Argv( 2 );

	if( *nick && *msg )
		CL_UIModule_MM_AddChatMsg( va( "%s%c%s", nick, 10, msg ) );
}

//================
// CL_MMC_AcknowledgePacket
// Clears an acknowledged packet from the packet list
//================
void CL_MMC_AcknowledgePacket( const socket_t *socket, const netadr_t *address )
{
	mm_packet_t *packet, *prev = NULL, *next;
	char *hash = Cmd_Argv( 1 );

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( Cmd_Argc() != 2 )
		return;

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

	// check for cmd in packet list
	for( packet = clmm_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
			clmm_packets = next;
	}
}

typedef struct
{
	char *name;
	void ( *func )( const socket_t *socket, const netadr_t *address );
	qboolean ack;
} mm_cmds_t;

static mm_cmds_t mm_cmds[] =
{
	{ "ack", CL_MMC_Ack, qfalse },
	{ "ackpacket", CL_MMC_AcknowledgePacket, qfalse },
	{ "add", CL_MMC_Add, qtrue },
	{ "chat", CL_MMC_Chat, qtrue },
	{ "drop", CL_MMC_Drop, qtrue },
#ifdef OLD_GAMESERVER_CHECKING
	{ "pingserver", CL_MMC_PingServer, qtrue },
#endif
	{ "shutdown", CL_MMC_Shutdown, qfalse },
	{ "start", CL_MMC_Start, qtrue },
	{ "status", CL_MMC_Status, qfalse },

	{ NULL, NULL }
};
//================
// CL_MM_Packets
// Handle matchmaking packets
//================
qboolean CL_MM_Packets( const socket_t *socket, const netadr_t *address, char *c )
{
	mm_cmds_t *cmd;

#ifndef ENABLE_MATCHMAKER
	return qfalse;
#endif

	if( !strcmp( NET_AddressToString( address ), cl_mmserver->string ) )
	{
		for( cmd = mm_cmds; cmd->name; cmd++ )
		{
			if( !strcmp( cmd->name, c ) )
			{
				// tell mm we have received the packet
				if( cmd->ack )
				{
					char hash[HASH_SIZE + 1];
					MM_Hash( hash, va( "%s %s", cmd, Cmd_Args() ) );
					CL_MM_SendMsgToServer( qfalse, "ackpacket %s", hash );
				}

				cmd->func( socket, address );

				return qtrue;
			}
		}
	}

	return qfalse;
}

//================
// CL_MM_Join
// Send a request to the server to join a match
//================
void CL_MM_Join( const char *gametype, const char *skill, const char *ping )
{
#ifndef ENABLE_MATCHMAKER
	return;
#endif

	// get match parameters
	igametype = MM_GetGameTypeTagByName( gametype );
	if( igametype == -1 )
		return;

	if( !strcmp( "any", skill ) )
		iskill = TYPE_ANY;
	else
		iskill = TYPE_DEPENDENT;

	if( !strcmp( ping, "any" ) )
		iping = TYPE_ANY;
	else
		iping = TYPE_DEPENDENT;

	mmpinged = cls.realtime;

	CL_UIModule_MM_UpdateStatus( "pinging matchmaker" );

	CL_MM_SendMsgToServer( qfalse, "ping join" );
}

//================
// CL_MM_Drop
// Send a drop message to the server and clear up match data
//================
void CL_MM_Drop( void )
{
	int i;

#ifndef ENABLE_MATCHMAKER
	return;
#endif

	if( mm_match )
	{
		CL_MM_SendMsgToServer( qfalse, "drop" );
		for( i = 0; i < mm_match->maxclients; i++ )
			CL_UIModule_MM_UpdateSlot( i, va( "Slot %d", i + 1 ) );
		MM_FreeMatch( mm_match );
		mm_match = NULL;
	}

	CL_MM_FreePackets();
}

//================
// CL_MM_Chat
// Send a chat message to the server
//================
void CL_MM_Chat( const char *msg, int type )
{
#ifndef ENABLE_MATCHMAKER
	return;
#endif

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

	if( !mm_match )
		return;

	CL_MM_SendMsgToServer( qtrue, "chat %d \"%s\"", type, msg );
}
