/*
   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"

static qboolean mm_initialized = qfalse;

socket_t socket_loopback_client;
socket_t socket_loopback_server;
socket_t socket_udp;
socket_t socket_tcp;

cvar_t *mm_ip;
cvar_t *mm_port;

mempool_t *mm_mempool;

unsigned long int realtime = 0;

//================
// MM_Init
// Initialize matchmaking components
//================
void MM_ShowPackets( void )
{
	mm_match_t *match;
	int i, count = 0;
	mm_packet_t *packet;

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

			Com_Printf( "%s:\n", match->clients[i]->nickname );
			for( packet = match->clients[i]->packets ; packet ; packet = packet->next )
			{
				count++;
				Com_Printf( " - %s\n", packet->cmd );
			}
		}
	}

	Com_Printf( "%d packet(s) in total\n", count );
}

void MM_Init( void )
{
	netadr_t address;

	assert( !mm_initialized );

	Cmd_AddCommand( "fakepacket", MM_FakePacket );
	Cmd_AddCommand( "matcheslist", MM_MatchesList );
	Cmd_AddCommand( "serverlist", MM_ServerList );
	Cmd_AddCommand( "clearmatches", MM_ClearMatches );
	Cmd_AddCommand( "showmmpackets", MM_ShowPackets );

	mm_ip = Cvar_Get( "mm_ip", "", CVAR_ARCHIVE );
	mm_port = Cvar_Get( "mm_port", "44000", CVAR_ARCHIVE );

	// networking
	NET_LoopbackAddress( &address );
	if( !NET_OpenSocket( &socket_loopback_client, SOCKET_LOOPBACK, &address, qtrue ) )
		Com_Error( ERR_FATAL, "Couldn't open loopback get socket: %s\n", NET_ErrorString() );
	if( !NET_OpenSocket( &socket_loopback_server, SOCKET_LOOPBACK, &address, qfalse ) )
		Com_Error( ERR_FATAL, "Couldn't open loopback send socket: %s\n", NET_ErrorString() );

	NET_StringToAddress( mm_ip->string, &address );
	address.port = BigShort( mm_port->integer );
	if( !NET_OpenSocket( &socket_udp, SOCKET_UDP, &address, qtrue ) )
		Com_Error( ERR_FATAL, "Couldn't open UDP socket: %s\n", NET_ErrorString() );
#ifdef TCP_SUPPORT
  if( !NET_OpenSocket( &socket_tcp, SOCKET_TCP, &address, qtrue ) )
    Com_Error( ERR_FATAL, "Couldn't open TCP socket: %s\n", NET_ErrorString() );
  else if( !NET_Listen( &socket_tcp ) )
    Com_Error( ERR_FATAL, "Couldn't listen to TCP socket: %s\n", NET_ErrorString() );
#endif

	DB_Init();
	Auth_Init();

	mm_mempool = Mem_AllocPool( NULL, "Matchmaker" );

	matchlist = ( mm_matchlist_t * ) MM_Malloc( sizeof( mm_matchlist_t ) );

	Cvar_Get( "masterservers", DEFAULT_MASTER_SERVERS_IPS, CVAR_ARCHIVE );

	//MM_ForceServerList();

	mm_initialized = qtrue;
}

//================
// MM_Shutdown
// Shutdown matchmaking components
//================
void MM_Shutdown( void )
{
	mm_match_t *match;

	if( !mm_initialized )
		return;

	Cmd_RemoveCommand( "matcheslist" );
	Cmd_RemoveCommand( "serverlist" );
	Cmd_RemoveCommand( "clearmatches" );

	DB_Shutdown();
	Auth_Shutdown();

	// tell any connected users we are shuting down
	match = matchlist->first;
	while( match )
	{
		MM_SendMsgToClients( qfalse, match, "shutdown" );
		match = match->next;
	}

	Mem_FreePool( &mm_mempool );

	NET_CloseSocket( &socket_loopback_client );
	NET_CloseSocket( &socket_loopback_server );
	NET_CloseSocket( &socket_udp );

	Cmd_RemoveCommand( "fakepacket" );

	realtime = 0;

	mm_initialized = qfalse;
}

//================
// MM_Frame
//================
void MM_Frame( const int realmsec )
{
	const unsigned int wrappingPoint = 0x70000000;

	realtime += realmsec;

	// mm has the pontential to wrap its realtime, because it should never reset.
	// in this event, we want to try and make sure clients dont notice
	if( realtime > wrappingPoint )
	{
		mm_match_t *match;
		mm_server_t *server;
		mm_packet_t *packet;
		int i = 0, j;

		// reset match times
		for( match = matchlist->first ; match ; i++, match = match->next ) 
		{
			match->lastping = i * 100; // seperate match pings out a bit
			match->awaitingserver = 1;
			for( j = 0 ; j < match->maxclients ; i++ )
			{
				if( !match->clients[i] )
					continue;

				match->clients[i]->lastack = i * 100 + 1;

				for( packet = match->clients[i]->packets ; packet ; packet = packet->next )
					packet->senttime = 0;
			}
		}

		i = 0;
		// reset server times
		for( server = serverlist ; server ; i++, server = server->next )
		{
			server->lastping = i * 100; // seperate server pings out a bit
			server->lastack = i * 100 + 1;
			server->cmd.senttime = 0;
		}
	}

	MM_ReadPackets();

	MM_PingClients();
	MM_CheckClientPackets();

	MM_CheckServerList();
	//MM_GetServerList( qfalse );
}


//================
// MS_ReadPackets
// Read packets and act accordingly
//================
void MM_ReadPackets( void )
{
	int i, ret;
	socket_t *socket = NULL;
#ifdef TCP_SUPPORT
  socket_t newsocket;
#endif
	netadr_t address;

	static msg_t msg;
	static qbyte msgData[MAX_MSGLEN];

	MSG_Init( &msg, msgData, sizeof( msgData ) );
	MSG_Clear( &msg );

#ifdef TCP_SUPPORT
  if( socket_tcp.open )
  {
    while( ( ret = NET_Accept( &socket_tcp, &newsocket, &address ) ) )
    {
		  if( ret == -1 )
		  {
			  Com_Printf( "NET_Accept: Error: %s\n", NET_ErrorString() );
			  continue;
		  }

      ret = NET_GetPacket( &newsocket, &address, &msg );
      if( ret == -1 )
		  {
			  Com_Printf( "NET_GetPacket: Error: %s\n", NET_ErrorString() );
			  continue;
		  }
      else if( ret == 1 )
      {
        if( *( int * )msg.data == -1 )
          MM_ConnectionlessPacket( &newsocket, &address, &msg );
      }
    }
  }
#endif

	for( i = 0; i < 2; i++ )
	{
		if( !i )
			socket = &socket_loopback_server;
		else if ( i == 1 )
			socket = &socket_udp;

		if( !socket->open )
			continue;

		while( ( ret = NET_GetPacket( socket, &address, &msg ) ) != 0 )
		{
			if( ret == -1 )
			{
				Com_Printf( "NET_GetPacket: Error: %s\n", NET_ErrorString() );
				continue;
			}

			// connectionless packet
			if( *(int *)msg.data == -1 )
				MM_ConnectionlessPacket( socket, &address, &msg );
		}
	}
}

//================
// MS_FakePacket
// Send a fake packet to itself
//================
void MM_FakePacket( void )
{
	netadr_t address;
	char *s = Cmd_Args();

	if( !s || !*s )
	{
		Com_Printf( "Usage: fakepacket <packet>\n" );
		return;
	}

	NET_LoopbackAddress( &address );

	Netchan_OutOfBandPrint( &socket_loopback_client, &address, "fake %s", s );

	Com_Printf( "Fake packet sent: %s\n", Cmd_Argv( 1 ) );
}
//================
// Supported connectionless packets
//================
typedef struct
{
	char *name;
	void ( *func )( const socket_t *socket, const netadr_t *address );
	qboolean ack;
} mm_cmd_t;

mm_cmd_t mm_cmds[] =
{
	// ignore info cmds ( we are not a gameserver )
	{ "info", NULL, qfalse },
	// matchmaking packets
	{ "ack", MMC_Acknowledge, qfalse },
	{ "ackpacket", MMC_AcknowledgePacket, qfalse },
	{ "chat", MMC_Chat, qtrue },
	{ "data", MMC_Data, qtrue },
	{ "drop", MMC_Drop, qfalse },
	{ "heartbeat", MMC_Heartbeat, qfalse },
	{ "join", MMC_Join, qtrue },
	{ "ping", MMC_Ping, qfalse },
#ifdef OLD_GAMESERVER_CHECKING
	{ "pingserver", MMC_PingServer, qfalse },
#endif
	{ "reply", MMC_Reply, qfalse },
	// authentication packets
	{ "auth", AuthC_Auth, qtrue },

	{ NULL, NULL }
};

//================
// MS_ConnectionlessPacket
// Handle connectionless packets
//================
#define HISTORY_SIZE 10
void MM_ConnectionlessPacket( const socket_t *socket, const netadr_t *address, msg_t *msg )
{
	mm_cmd_t *cmd;
	char *s, *c, *pdelim;
	static int history[HISTORY_SIZE];
	static int hindex = 0;
	int pno = 0;

	MSG_BeginReading( msg );
	MSG_ReadLong( msg ); // ignore -1

	s = MSG_ReadStringLine( msg );

	// check for reliability packet number
	if( ( pdelim = strchr( s, PACKET_NO_DELIM ) ) )
	{
		int i;

		*pdelim = 0;
		pno = atoi( pdelim + 1 );
		
		for( i = 0 ; i < HISTORY_SIZE ; i++ )
		{
			// this packet has already been received
			if( history[i] == pno )
				return;
		}
		// store this packet number
		hindex = ( hindex + 1 ) % ( HISTORY_SIZE - 1 );
		history[hindex] = pno;
	}

	Cmd_TokenizeString( s );
	c = Cmd_Argv( 0 );

	if( !strcmp( c, "fake" ) )
	{
		// fake packets will only ever be sent on loopback
		if( address->type != NA_LOOPBACK || socket->type != SOCKET_LOOPBACK )
			return;

		// hackish to remove fake from arg list
		Cmd_TokenizeString( va( "%s", Cmd_Args() ) );

		Com_DPrintf( "Fake packet recieved: %s\n", c );
	}
	else
		Com_DPrintf( "Packet from %s: %s\n", NET_AddressToString( address ), c );


	for( cmd = mm_cmds; cmd->name; cmd++ )
	{
		if( !strcmp( cmd->name, c ) )
		{
			// send acknowledgement of packet being received
			//if( cmd->ack )
			// if this packet has a packet number, send an acknowledgement to mm
			if( pno )
			{
				char hash[HASH_SIZE + 1];
				MM_Hash( hash, va( "%s %s", c, Cmd_Args() ) );
				Netchan_OutOfBandPrint( socket, address, "ackpacket %s", hash );
			}

			if( cmd->func )
				cmd->func( socket, address );

			return;
		}
	}

	Com_Printf( "Bad connectionless packet from %s:\n%s\n", NET_AddressToString( address ), s );
}
