/*
   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 "tv_local.h"

#include "tv_relay.h"

#include "tv_upstream.h"
#include "tv_relay_parse.h"
#include "tv_relay_module.h"
#include "tv_relay_client.h"
#include "tv_downstream.h"

#include <setjmp.h>

// for jumping over relay handling when it's disconnected
jmp_buf relay_abortframe;

//================
//TV_Relay_RunSnap
//================
static qboolean TV_Relay_RunSnap( relay_t *relay )
{
	int start, i;

	if( relay->state != CA_ACTIVE )
		return qfalse;

	if( !relay->lastFrame || !relay->lastFrame->valid )
		return qfalse;

	if( relay->curFrame == relay->lastFrame )
		return qfalse;

	if( !relay->map_checksum )
		return qfalse; // not fully loaded yet

	if( relay->curFrame && relay->curFrame->valid )
	{
		start = relay->curFrame->serverFrame + 1;
		if( start < relay->lastFrame->serverFrame - UPDATE_BACKUP + 1 )
			start = relay->lastFrame->serverFrame - UPDATE_BACKUP + 1;
	}
	else
	{
		start = relay->lastFrame->serverFrame - UPDATE_BACKUP + 1;
	}

	// find the frame
	for( i = start; i <= relay->lastFrame->serverFrame; i++ )
	{
		if( relay->frames[i & UPDATE_MASK].valid && relay->frames[i & UPDATE_MASK].serverFrame == i )
		{
			// we buffer server snaps and launch them with slight delay to add smoothness
			if( relay->serverTime < relay->frames[i & UPDATE_MASK].serverTime + relay->snapFrameTime )
				return qfalse;

			relay->curFrame = &relay->frames[i & UPDATE_MASK];
			relay->framenum = relay->curFrame->serverFrame;

			return qtrue;
		}
	}
	assert( qfalse ); // lastFrame has to match atleast

	return qfalse;
}

//================
//TV_Relay_InitMap
//================
void TV_Relay_InitMap( relay_t *relay )
{
	int i;

	// load the map
	CM_LoadMap( relay->cms, relay->configstrings[CS_MODELS+1], qfalse, &relay->map_checksum );

	// allow different map checksums for demos
	if( !relay->upstream->demoplaying )
		if( (unsigned)atoi( relay->configstrings[CS_MAPCHECKSUM] ) != relay->map_checksum )
			TV_Relay_Error( relay, "Local map version differs from server: %u != '%u'",
							   relay->map_checksum, (unsigned)atoi( relay->configstrings[CS_MAPCHECKSUM] ) );

	// load and spawn all other entities
	relay->module_export->SpawnEntities( relay->module, relay->configstrings[CS_MODELS+1],
	                                    CM_EntityString( relay->cms ), CM_EntityStringLen( relay->cms ) );

	for( i = 0; i < MAX_CONFIGSTRINGS; i++ )
		relay->module_export->ConfigString( relay->module, i, relay->configstrings[i] );
}

//=================
//TV_Relay_Error
// Must only be called from inside TV_Relay_Run
//=================
void TV_Relay_Error( relay_t *relay, const char *format, ... )
{
	va_list	argptr;
	char msg[1024];

	assert( relay );
	assert( format );

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

	TV_Relay_Shutdown( relay, "%s", msg );
	longjmp( relay_abortframe, -1 );
}

//=================
//TV_Relay_Shutdown
//=================
void TV_Relay_Shutdown( relay_t *relay, const char *format, ... )
{
	va_list	argptr;
	char msg[1024], cmd[256];
	int i;
	client_t *client;

	if( relay->state == CA_UNINITIALIZED )
		return;

	assert( relay );
	assert( format );

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

	Com_Printf( "%s: Relay shutdown: %s\n", relay->upstream->name, msg );

	// send a message to each connected client
	for( i = 0, client = tvs.clients; i < tv_maxclients->integer; i++, client++ )
	{
		if( client->relay != relay )
			continue;
		TV_Downstream_ChangeStream( client, NULL );
	}

	if( relay->state > CA_CONNECTING )
	{
		for( i = 0; i < tvs.numupstreams; i++ )
		{
			if( tvs.upstreams[i] == relay->upstream )
				break;
		}
		assert( i < tvs.numupstreams );

		Q_snprintfz( cmd, sizeof( cmd ), "chr %i", i+1 );

		for( i = 0, client = tvs.clients; i < tv_maxclients->integer; i++, client++ )
		{
			if( client->state != CS_SPAWNED )
				continue;

			TV_Downstream_AddGameCommand( client->relay, client, cmd );
		}
	}

	if( relay->module_export )
		TV_Relay_ShutdownModule( relay );

	if( relay->client_entities )
		Mem_Free( relay->client_entities );

	CM_Free( relay->cms );
	relay->cms = NULL;

	relay->state = CA_UNINITIALIZED;
}

/*
   =====================
   TV_Relay_ClearState
   =====================
 */
void TV_Relay_ClearState( relay_t *relay )
{
	memset( relay->configstrings, 0, sizeof( relay->configstrings ) );
	memset( relay->baselines, 0, sizeof( relay->baselines ) );

	relay->lastFrame = NULL;
	relay->lastUcmdFrame = NULL;
	relay->curFrame = NULL;
	memset( relay->frames, 0, sizeof( relay->frames ) );
	relay->framenum = 0;
	relay->lastExecutedServerCommand = 0;

	relay->numframesounds = 0;
}

/*
   =================
   TV_Relay_GetPacket
   =================
 */
static msg_t *TV_Relay_GetPacket( relay_t *relay )
{
	packet_t *packet;

	assert( relay && relay->state > CA_UNINITIALIZED );

	if( !relay->packetqueue_pos )
		packet = relay->upstream->packetqueue;
	else
		packet = relay->packetqueue_pos->next;

	if( packet && ( tvs.realtime >= relay->delay ) && ( packet->time + relay->delay < tvs.realtime ) )
	{
		relay->packetqueue_pos = packet;
		return &packet->msg;
	}

	return NULL;
}

//==================
//TV_Relay_NameNotify
//==================
void TV_Relay_NameNotify( relay_t *relay, client_t *client )
{
	char cmd[256];

	Q_snprintfz( cmd, sizeof( cmd ), "cha %i \"%s\" \"%s\"", relay->upstream->number + 1, relay->configstrings[CS_HOSTNAME],
		NET_AddressToString( &relay->upstream->serveraddress ) );

	if( client )
	{
		TV_Downstream_AddGameCommand( client->relay, client, cmd );
	}
	else
	{
		int i;

		for( i = 0, client = tvs.clients; i < tv_maxclients->integer; i++, client++ )
		{
			if( client->state != CS_SPAWNED )
				continue;
			TV_Downstream_AddGameCommand( client->relay, client, cmd );
		}
	}
}

/*
   =================
   TV_Relay_ReadPackets
   =================
 */
static void TV_Relay_ReadPackets( relay_t *relay )
{
	msg_t *msg;

	assert( relay && relay->state > CA_UNINITIALIZED );

	while( ( msg = TV_Relay_GetPacket( relay ) ) != NULL )
	{
		if( relay->state == CA_CONNECTING )
		{
			int i;

			for( i = 0; i < tvs.numupstreams; i++ )
			{
				if( tvs.upstreams[i] == relay->upstream )
					break;
			}
			assert( i < tvs.numupstreams );

			relay->state = CA_HANDSHAKE;
		}

		/*if( *(int *)msg->data == -1 ) // remote command packet
		   {
		    TV_Relay_UpstreamlessPacket( relay, msg );
		   }
		   else
		   {*/
		// skip header
		MSG_BeginReading( msg );
		if( !relay->upstream->demoplaying )
		{
			MSG_ReadLong( msg );
			MSG_ReadLong( msg );
		}
		if( relay->state >= CA_HANDSHAKE )
			TV_Relay_ParseServerMessage( relay, msg );
		//}
	}
}

//================
//TV_Relay_Run
//================
void TV_Relay_Run( relay_t *relay )
{
	if( setjmp( relay_abortframe ) )  // disconnect while running
		return;

	relay->serverTime = tvs.realtime + relay->serverTimeDelta;

	TV_Relay_ReadPackets( relay );
	if( relay->state <= CA_DISCONNECTED )
		return;

	if( TV_Relay_RunSnap( relay ) )
	{
		relay->module_export->RunFrame( relay->module, tvs.realtime - relay->lastrun );
		relay->lastrun = tvs.realtime;

		relay->module_export->NewFrameSnapshot( relay->module, relay->curFrame );
		relay->module_export->SnapFrame( relay->module );

		TV_Relay_SendClientMessages( relay );

		relay->module_export->ClearSnap( relay->module );
		relay->numframesounds = 0;
	}

	if( relay->upstream->state == CA_DISCONNECTED && relay->packetqueue_pos == relay->upstream->packetqueue_head )
		TV_Relay_Shutdown( relay, "Out of data" );
}

//================
//TV_Relay_UpstreamUserinfoChanged
//================
void TV_Relay_UpstreamUserinfoChanged( relay_t *relay )
{
	relay->upstream->userinfo_modified = qtrue;
}

//================
//TV_Relay_Init
//================
void TV_Relay_Init( relay_t *relay, upstream_t *upstream, int delay )
{
	assert( relay );
	assert( upstream );

	Com_Printf( "%s: Relay init\n", upstream->name );

	memset( relay, 0, sizeof( *relay ) );

	relay->state = CA_CONNECTING;
	relay->upstream = upstream;

	relay->max_clients = MAX_CLIENTS;

	relay->num_client_entities = tv_maxclients->integer * UPDATE_BACKUP * MAX_SNAP_ENTITIES;
	relay->client_entities = Mem_Alloc( upstream->mempool, sizeof( entity_state_t ) * relay->num_client_entities );

	relay->cms = CM_New( upstream->mempool );

	relay->delay = max( delay, RELAY_MIN_DELAY );
}
