/*
Copyright (C) 2003-2006 Andrey Nazarov

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.

*/

//
// cl_demo.c - demo recording and playback
//

#include "client.h"

cvar_t	*cl_demo_local_fov;

static cvar_t *cl_demo_timescale;

// =========================================================================

/*
====================
CL_WriteDemoMessage

Dumps the current net message, prefixed by the length
====================
*/
void CL_WriteDemoMessage( sizebuf_t *msgbuf, int bytesToSkip ) {
	int		len, swlen;

	// the first several bytes are just packet sequencing stuff
	len = msgbuf->cursize - bytesToSkip;
	if( len < 1 ) {
		return; // don't write bad messages
	}

	swlen = LittleLong( len );
	FS_Write( &swlen, 4, cls.demofile );
	FS_Write( msgbuf->data + bytesToSkip, len, cls.demofile );
}


/*
====================
CL_CloseDemoFile
====================
*/
void CL_CloseDemoFile( void ) {
	if( !cls.demofile ) {
		return;
	}

	FS_FCloseFile( cls.demofile );
	
	cls.demofile = 0;
}


/*
====================
CL_Stop_f

stop recording a demo
====================
*/
void CL_Stop_f( void ) {
	int len;

	if( !cls.demorecording ) {
		Com_Printf( "Not recording a demo.\n" );
		return;
	}

	if( cls.serverProtocol == PROTOCOL_VERSION_R1Q2 ||
		cls.serverProtocol == PROTOCOL_VERSION_Q2PRO )
	{
		/* tell the server we finished recording */
		MSG_WriteByte( clc_setting );
		MSG_WriteShort( CLS_RECORDING );
		MSG_WriteShort( 0 );
		MSG_FlushTo( &cls.netchan->message );
	}

// finish up
	len = -1;
	FS_Write( &len, 4, cls.demofile );

	CL_CloseDemoFile();

	cls.demorecording = qfalse;
	Com_Printf( "Stopped demo.\n" );
}

/*
====================
CL_Record_f

record <demoname>

Begins recording a demo from the current position
====================
*/
void CL_Record_f( void ) {
	char	name[MAX_QPATH];
	int		i, length;
	entity_state_t	*ent;
	char *string;
	fileHandle_t demofile;
	qboolean compressed = qfalse;

	i = 1;
	if( !strcmp( Cmd_Argv( i ), "-c" ) || !strcmp( Cmd_Argv( i ), "--compressed" ) ) {
		compressed = qtrue;
		i++;
	}

	if( i >= Cmd_Argc() ) {
		Com_Printf( "Usage: %s [-c|--compressed] [/]<filename>\n", Cmd_Argv( 0 ) );
		return;
	}

	if( cls.demorecording ) {
		Com_Printf( "Already recording.\n" );
		return;
	}

	if( cls.state != ca_active || cls.demoplayback ) {
		Com_Printf( "You must be in a level to record.\n" );
		return;
	}

	//
	// open the demo file
	//
	string = Cmd_Argv( i );
	if( *string == '/' ) {
		Q_strncpyz( name, string + 1, sizeof( name ) );
	} else {
		Com_sprintf( name, sizeof( name ), "demos/%s", string );
		if( cls.serverProtocol == PROTOCOL_VERSION_R1Q2 ) {
			COM_DefaultExtension( name, ".dm_35", sizeof( name ) );
		} else if( cls.serverProtocol == PROTOCOL_VERSION_Q2PRO ) {
			COM_DefaultExtension( name, ".dm_36", sizeof( name ) );
		} else {
			COM_DefaultExtension( name, ".dm2", sizeof( name ) );
		}
	}
	if( compressed ) {
		Q_strcat( name, sizeof( name ), ".gz" );
	}

	FS_FOpenFile( name, &demofile, FS_MODE_WRITE );
	if( !demofile ) {
		Com_EPrintf( "Couldn't open %s for writing.\n", name );
		return;
	}

	Com_Printf( "Recording client demo to %s.\n", name );

	cls.demofile = demofile;
	cls.demorecording = qtrue;

	// don't start saving messages until a non-delta compressed message is received
	cls.demowaiting = qtrue;

	if( cls.serverProtocol == PROTOCOL_VERSION_R1Q2 ||
		cls.serverProtocol == PROTOCOL_VERSION_Q2PRO )
	{
		/* tell the server we are recording */
		MSG_WriteByte( clc_setting );
		MSG_WriteShort( CLS_RECORDING );
		MSG_WriteShort( 1 );
		MSG_FlushTo( &cls.netchan->message );
	}

	//
	// write out messages to hold the startup information
	//

	// send the serverdata
	MSG_WriteByte( svc_serverdata );
	MSG_WriteLong( cls.serverProtocol );
	MSG_WriteLong( 0x10000 + cl.servercount );
	MSG_WriteByte( ATR_DEMO );	// demos are always attract loops
	MSG_WriteString( cl.gamedir );
    if( cl.clientNum == CLIENTNUM_NONE ) {
        MSG_WriteShort( MAX_EDICTS - 1 );
    } else {
    	MSG_WriteShort( cl.clientNum );
    }

	MSG_WriteString( cl.configstrings[CS_NAME] );

	if( cls.serverProtocol == PROTOCOL_VERSION_R1Q2 ) {
		MSG_WriteByte( 0 );		/* enchanced */
		MSG_WriteShort( PROTOCOL_VERSION_R1Q2_MINOR );
		MSG_WriteByte( 0 );		/* advanced deltas */
		MSG_WriteByte( cl.pmp.strafeHack );
	} else if( cls.serverProtocol == PROTOCOL_VERSION_Q2PRO ) {
		MSG_WriteShort( PROTOCOL_VERSION_Q2PRO_MINOR );
		MSG_WriteByte( cl.gametype );
		MSG_WriteByte( cl.pmp.strafeHack );
		MSG_WriteByte( cl.pmp.qwmod );
	}

	// configstrings
	for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
		string = cl.configstrings[i];
		if( !string[0] ) {
			continue;
		}

		length = strlen( string );
		if( length > MAX_QPATH ) {
			length = MAX_QPATH;
		}
		
		if( cls.serverProtocol != PROTOCOL_VERSION_Q2PRO ) {
			if( msg_write.cursize + length + 4 > MAX_PACKETLEN ) {
				CL_WriteDemoMessage( &msg_write, 0 );
				SZ_Clear( &msg_write );
			}
		}

		MSG_WriteByte( svc_configstring );
		MSG_WriteShort( i );
		MSG_WriteData( string, length );
		MSG_WriteByte( 0 );

	}

	// baselines
	for( i = 1; i < MAX_EDICTS; i++ ) {
		ent = &cl.baselines[i];
		if( !ent->number ) {
			continue;
		}

		if( cls.serverProtocol != PROTOCOL_VERSION_Q2PRO ) {
			if( msg_write.cursize + 64 > MAX_PACKETLEN ) {
				CL_WriteDemoMessage( &msg_write, 0 );
				SZ_Clear( &msg_write );
			}
		}

		MSG_WriteByte( svc_spawnbaseline );		
		MSG_WriteDeltaEntity( NULL, ent, MSG_ES_FORCE );
	}

	MSG_WriteByte( svc_stufftext );
	MSG_WriteString( "precache\n" );

	// write it to the demo file
	CL_WriteDemoMessage( &msg_write, 0 );
	SZ_Clear( &msg_write );

	// the rest of the demo file will be individual frames
}

/*
====================
CL_ReadNextDemoMessage
====================
*/
static qboolean CL_ReadNextDemoMessage( fileHandle_t f ) {
	int		msglen;

	// read msglen
	if( FS_Read( &msglen, 4, f ) != 4 ) {
		return qfalse;
	}

	if( msglen == -1 ) {
		return qfalse;
	}

	msglen = LittleLong( msglen );
	if( msglen < 1 || msglen >= msg_read.maxsize ) {
		return qfalse;
	}

	msg_read.cursize = msglen;
	msg_read.readcount = 0;

	// read packet data
	if( FS_Read( msg_read.data, msglen, f ) != msglen ) {
		return qfalse;
	}

	return qtrue;

}

/*
====================
CL_ParseNextDemoMessage
====================
*/
static void CL_ParseNextDemoMessage( void ) {
	int pos;
	char *s;

	if( !CL_ReadNextDemoMessage( cls.demofile ) ) {
		s = Cvar_VariableString( "nextserver" );
		if( !s[0] ) {
			Com_Error( ERR_SILENT, "Demo finished" );
		}
		Cbuf_AddText( s );
		Cbuf_AddText( "\n" );
		Cvar_Set( "nextserver", "" );
		cls.state = ca_connected;
		return;
	}

	CL_ParseServerMessage();

	if( cls.demofileSize ) {
		pos = FS_Tell( cls.demofile ) - cls.demofileFrameOffset;
		if( pos < 0 ) {
			pos = 0;
		}
		cls.demofilePercent = pos * 100 / cls.demofileSize;
	}
}

static const char *demoExtTable[] = {
	".dm2",
	".dm2.gz",
	".dm_35",
	".dm_35.gz",
	".dm_36",
	".dm_36.gz"
};

static const int numDemoExts = sizeof( demoExtTable ) / sizeof( demoExtTable[0] );

/*
====================
CL_PlayDemo_f
====================
*/
static void CL_PlayDemo_f( void ) {
	char name[MAX_QPATH];
	fileHandle_t demofile;
	char *arg, *ext;
	int i, length;

	if( Cmd_Argc() < 2 ) {
		Com_Printf( "Usage: %s <filename>\n", Cmd_Argv( 0 ) );
		return;
	}

	demofile = 0;
	length = 0;

	arg = Cmd_Argv( 1 );

	if( arg[0] == '/' ) {
		// Assume full path is given
		Q_strncpyz( name, arg + 1, sizeof( name ) );
		FS_FOpenFile( name, &demofile, FS_MODE_READ );
	} else {
		// Search for matching extensions
		ext = COM_FileExtension( arg );
		if( *ext ) {
			Com_sprintf( name, sizeof( name ), "demos/%s", arg );
			Com_Printf( "Trying %s...\n", name );
			FS_FOpenFile( name, &demofile, FS_MODE_READ );	
		} else {
			// Try all known extensions
			for( i = 0; i < numDemoExts; i++ ) {
				Com_sprintf( name, sizeof( name ), "demos/%s%s", arg, demoExtTable[i] );
				Com_Printf( "Trying %s...\n", name );

				FS_FOpenFile( name, &demofile, FS_MODE_READ );
				if( demofile ) {
					break;
				}
			}
		}
	}

	if( !demofile ) {
		Com_Printf( "Couldn't open demofile\n" );
		return;
	}

	if( sv_running->integer ) {
		// if running a local server, kill it and reissue
		SV_Shutdown( "Server was killed\n", KILL_DROP );
	}

	CL_Disconnect( ERR_DISCONNECT, NULL );
	
	Con_Close();

	cls.demofile = demofile;
	
	cls.demoplayback = qtrue;
	cls.state = ca_connected;
	Q_strncpyz( cls.servername, COM_SkipPath( name ), sizeof( cls.servername ) );
	cls.serverAddress.type = NA_LOOPBACK;

	SCR_UpdateScreen();

	do {
		CL_ParseNextDemoMessage();
	    Cbuf_Execute();
	} while( cls.state == ca_connected );

	length = FS_GetFileLengthNoCache( cls.demofile );
	cls.demofileFrameOffset = FS_Tell( cls.demofile );
	cls.demofileSize = length - cls.demofileFrameOffset;

	if( com_timedemo->integer ) {
		cls.timeDemoFrames = 0;
		cls.timeDemoStart = Sys_Milliseconds();
	}
}

static const char *CL_PlayDemo_g( const char *partial, int state ) {
	return Com_FileNameGeneratorByFilter( "demos", "*.dm2;*.dm_3?;*.dm2.gz;*.dm_3?.gz", partial, qfalse, state );
}

/*
====================
CL_GetDemoInfo
====================
*/
qboolean CL_GetDemoInfo( const char *path, demoInfo_t *info ) {
	fileHandle_t hFile;
	int c, protocol;
	char *s, *p;

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

	FS_FOpenFile( path, &hFile, FS_MODE_READ );
	if( !hFile ) {
		return qfalse;
	}

	if( !CL_ReadNextDemoMessage( hFile ) ) {
		goto fail;
	}

	if( MSG_ReadByte() != svc_serverdata ) {
		goto fail;
	}

	protocol = MSG_ReadLong();

	msg_read.readcount += 5;

	Q_strncpyz( info->gamedir, MSG_ReadString(), sizeof( info->gamedir ) );

	info->clientNum = MSG_ReadShort();

	Q_strncpyz( info->fullLevelName, MSG_ReadString(), sizeof( info->fullLevelName ) );

	switch( protocol ) {
	case PROTOCOL_VERSION_MVD:
		info->mvd = qtrue;
		msg_read.readcount += 2;
		break;
	case PROTOCOL_VERSION_R1Q2:
		msg_read.readcount += 5;
		break;
	case PROTOCOL_VERSION_Q2PRO:
		msg_read.readcount += 5;
		break;
	default:
		break;
	}

	while( 1 ) {
		c = MSG_ReadByte();
		if( c == -1 ) {
			if( !CL_ReadNextDemoMessage( hFile ) ) {
				break;
			}
			continue; // parse new message
		}
		if( c != svc_configstring ) {
			break;
		}
		c = MSG_ReadShort();
		s = MSG_ReadString();
		if( c >= CS_PLAYERSKINS && c < CS_PLAYERSKINS + MAX_DEMOINFO_CLIENTS ) {
			c -= CS_PLAYERSKINS;
			Q_strncpyz( info->clients[c], s, sizeof( info->clients[0] ) );
			if( ( p = strchr( info->clients[c], '\\' ) ) != NULL ) {
				*p = 0;
			}
		} else if( c == CS_MODELS + 1 ) {
			if( strlen( s ) > 9 ) {
				Q_strncpyz( info->mapname, s + 5, sizeof( info->mapname ) ); // skip "maps/"
				info->mapname[ strlen( info->mapname ) - 4 ] = 0; // cut off ".bsp"
			}
		}
	}

	FS_FCloseFile( hFile );
	return qtrue;

fail:
	FS_FCloseFile( hFile );
	return qfalse;

}

// =========================================================================


/*
====================
CL_DemoFrame
====================
*/
void CL_DemoFrame( int msec ) {
	static float frac;
	int dt;

	if( cls.state < ca_connected ) {
		return;
	}

	if( sv_paused->integer ) {
		return;
	}

	if( !cls.demoplayback ) {
		cl.time += msec;
		return;
	}

	if( cls.state != ca_active ) {
		CL_ParseNextDemoMessage();
		return;
	}

	if( com_timedemo->integer ) {
		CL_ParseNextDemoMessage();
		cl.time = cl.serverTime;
		cls.timeDemoFrames++;
		return;
	}

	if( cl_demo_timescale->value < 0 ) {
		Cvar_Set( "cl_demo_timescale", "0" );
	} else if( cl_demo_timescale->value > 1000 ) {
		Cvar_Set( "cl_demo_timescale", "1000" );
	}

	if( cl_demo_timescale->value ) {
		frac += msec * cl_demo_timescale->value;
		dt = frac;
		frac -= dt;

		cl.time += dt;
	}

	while( cl.serverTime < cl.time ) {
		CL_ParseNextDemoMessage();
		if( cls.state != ca_active ) {
			break;
		}
	}

}

static void CL_DemoLocalFov_OnChange( cvar_t *self, void *arg ) {
	CL_UpdateLocalFovSetting();
}

/*
====================
CL_InitDemos
====================
*/
void CL_InitDemos( void ) {
	cl_demo_timescale = Cvar_Get( "cl_demo_timescale", "1", 0 );
	cl_demo_local_fov = Cvar_Get( "cl_demo_local_fov", "1", 0 );
	cl_demo_local_fov->changedFunc = CL_DemoLocalFov_OnChange;

	Cmd_AddCommandEx( "demo", CL_PlayDemo_f, CL_PlayDemo_g );
	Cmd_AddCommandEx( "record", CL_Record_f, CL_PlayDemo_g );
	Cmd_AddCommand( "stop", CL_Stop_f );	
}


