//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
//===========================================================================//

#include <tier0/dbg.h>
#include <tier1/strtools.h>
#include <utlbuffer.h>

#include "demofile.h"
#include "filesystem_engine.h"
#include "demo.h"
#include "proto_version.h"
#include "convar.h"	// For dbg_demofile

// NOTE: This has to be the last file included!
#include "tier0/memdbgon.h"


void Host_EndGame (bool bShowMainMenu, const char *message, ...);

// Debug helpers - this class prints in a nested format
ConVar dbg_demofile( "dbg_demofile", "0", FCVAR_DEVELOPMENTONLY | FCVAR_HIDDEN );
//#define DEMOFILE_DBG_PRINT
#if defined( DEMOFILE_DBG_PRINT )
class CDbgPrint
{
public:
	static int s_nIndent;
	CDbgPrint( const char *pMsg )
	{
		++s_nIndent;
		if ( dbg_demofile.GetInt() )
		{
			for (int i = 0; i < 3*s_nIndent; ++i)
				DevMsg(" ");
			DevMsg( pMsg );
		}
	}
	~CDbgPrint() { --s_nIndent; }
};
int CDbgPrint::s_nIndent = 0;
#define DemoFileDbg(_txt) CDbgPrint printer( _txt )
#else
#define DemoFileDbg(_txt) (void)0
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CDemoFile::CDemoFile() :
	m_pBuffer( NULL ),
	m_bAllowHeaderWrite( true ),
	m_bIsStreamBuffer( false )
{
}

CDemoFile::~CDemoFile()
{
	if ( IsOpen() )
	{
		Close();
	}
}

void CDemoFile::WriteSequenceInfo(int nSeqNrIn, int nSeqNrOut)
{
	DemoFileDbg( "WriteSequenceInfo()\n" );
	Assert( m_pBuffer && m_pBuffer->IsValid() );
	m_pBuffer->PutInt( nSeqNrIn );
	m_pBuffer->PutInt( nSeqNrOut );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CDemoFile::ReadSequenceInfo(int &nSeqNrIn, int &nSeqNrOut)
{
	Assert( m_pBuffer && m_pBuffer->IsValid() );
	nSeqNrIn = m_pBuffer->GetInt( );
	nSeqNrOut = m_pBuffer->GetInt( );
}


inline void ByteSwap_democmdinfo_t( democmdinfo_t &swap )
{
	swap.flags = LittleDWord( swap.flags );

	LittleFloat( &swap.viewOrigin.x, &swap.viewOrigin.x );
	LittleFloat( &swap.viewOrigin.y, &swap.viewOrigin.y );
	LittleFloat( &swap.viewOrigin.z, &swap.viewOrigin.z );

	LittleFloat( &swap.viewAngles.x, &swap.viewAngles.x );
	LittleFloat( &swap.viewAngles.y, &swap.viewAngles.y );
	LittleFloat( &swap.viewAngles.z, &swap.viewAngles.z );

	LittleFloat( &swap.localViewAngles.x, &swap.localViewAngles.x );
	LittleFloat( &swap.localViewAngles.y, &swap.localViewAngles.y );
	LittleFloat( &swap.localViewAngles.z, &swap.localViewAngles.z );

	LittleFloat( &swap.viewOrigin2.x, &swap.viewOrigin2.x );
	LittleFloat( &swap.viewOrigin2.y, &swap.viewOrigin2.y );
	LittleFloat( &swap.viewOrigin2.z, &swap.viewOrigin2.z );

	LittleFloat( &swap.viewAngles2.x, &swap.viewAngles2.x );
	LittleFloat( &swap.viewAngles2.y, &swap.viewAngles2.y );
	LittleFloat( &swap.viewAngles2.z, &swap.viewAngles2.z );

	LittleFloat( &swap.localViewAngles2.x, &swap.localViewAngles2.x );
	LittleFloat( &swap.localViewAngles2.y, &swap.localViewAngles2.y );
	LittleFloat( &swap.localViewAngles2.z, &swap.localViewAngles2.z );
}

void CDemoFile::WriteCmdInfo( democmdinfo_t& info )
{
	DemoFileDbg( "WriteCmdInfo()\n" );
	democmdinfo_t littleEndianInfo = info;
	ByteSwap_democmdinfo_t( littleEndianInfo );

	Assert( m_pBuffer && m_pBuffer->IsValid() );
	m_pBuffer->Put( &littleEndianInfo, sizeof(democmdinfo_t) );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CDemoFile::ReadCmdInfo( democmdinfo_t& info )
{
	Assert( m_pBuffer && m_pBuffer->IsValid() );
	m_pBuffer->Get( &info, sizeof(democmdinfo_t) );

	ByteSwap_democmdinfo_t( info );
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : cmd - 
//			*fp - 
//-----------------------------------------------------------------------------
void CDemoFile::WriteCmdHeader( unsigned char cmd, int tick )
{
	if ( dbg_demofile.GetInt() ) DevMsg( "----------------------------------------\n" );
	Assert( cmd >= dem_signon && cmd <= dem_lastcmd );

	Assert( m_pBuffer && m_pBuffer->IsValid() );
	m_pBuffer->PutUnsignedChar( cmd );
	m_pBuffer->PutInt( tick );

	static const char *cmdname[] = 
	{
		"dem_unknown",
		"dem_signon",
		"dem_packet",
		"dem_synctick",
		"dem_consolecmd",
		"dem_usercmd",
		"dem_datatables",
		"dem_stop",
		"dem_stringtables"
	};

	DemoFileDbg( "WriteCmdHeader()..." );
	if ( dbg_demofile.GetInt() ) DevMsg( "tick %i, cmd %s \n", tick, cmdname[cmd] );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : cmd - 
//			dt - 
//			frame - 
//-----------------------------------------------------------------------------
void CDemoFile::ReadCmdHeader( unsigned char& cmd, int& tick )
{
	Assert( m_pBuffer && m_pBuffer->IsValid() );
	cmd = m_pBuffer->GetUnsignedChar( );
	if ( !m_pBuffer || !m_pBuffer->IsValid() )
	{
		ConDMsg("Missing end tag in demo file.\n");
		cmd = dem_stop;
		return;
	}

	if ( cmd <= 0 || cmd > dem_lastcmd )
	{
		ConDMsg("Unexepcted command token [%d] in .demo file\n", cmd );
		cmd = dem_stop;
		return;
	}

	tick = m_pBuffer->GetInt( );
}

void CDemoFile::WriteConsoleCommand( const char *cmdstring, int tick )
{
	DemoFileDbg( "WriteConsoleCommand()\n" );
	if ( !cmdstring || !cmdstring[0] )
		return;

	if ( !m_pBuffer || !m_pBuffer->IsValid() )
		return;

	int len = Q_strlen( cmdstring ) + 1;
	if ( len >= 1024 )
	{
		DevMsg("CDemoFile::WriteConsoleCommand: command too long (>1024).\n");
		return;
	}

	WriteCmdHeader( dem_consolecmd, tick );

	WriteRawData( cmdstring, len );
}

const char *CDemoFile::ReadConsoleCommand()
{
	static char cmdstring[1024];
	
	ReadRawData( cmdstring, sizeof(cmdstring) );

	return cmdstring;
}

unsigned int CDemoFile::GetCurPos( bool bRead )
{
	if ( !m_pBuffer || !m_pBuffer->IsValid() )
		return 0;
	if ( bRead )
		return m_pBuffer->TellGet();
	return m_pBuffer->TellPut();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : buf - 
//-----------------------------------------------------------------------------
void CDemoFile::WriteNetworkDataTables( bf_write *buf, int tick  )
{
	DemoFileDbg( "WriteNetworkDataTables()\n" );
	MEM_ALLOC_CREDIT();

	if ( !m_pBuffer || !m_pBuffer->IsValid() )
	{
		DevMsg("CDemoFile::WriteNetworkDataTables: Haven't opened file yet!\n" );
		return;
	}

	WriteCmdHeader( dem_datatables, tick );

	WriteRawData( (char*)buf->GetBasePointer(), buf->GetNumBytesWritten() );
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : expected_length - 
//			&demofile - 
//-----------------------------------------------------------------------------
int CDemoFile::ReadNetworkDataTables( bf_read *buf )
{
	if ( buf )
		return ReadRawData( (char*)buf->GetBasePointer(), buf->GetNumBytesLeft() );
	return ReadRawData( NULL, 0 ); // skip data
}

void CDemoFile::WriteStringTables( bf_write *buf, int tick )
{
	DemoFileDbg( "WriteStringTables()\n" );
	MEM_ALLOC_CREDIT();

	if ( !m_pBuffer || !m_pBuffer->IsValid() )
	{
		DevMsg("CDemoFile::WriteStringTables: Haven't opened file yet!\n" );
		return;
	}

	WriteCmdHeader( dem_stringtables, tick );

	WriteRawData( (char*)buf->GetBasePointer(), buf->GetNumBytesWritten() );
}

int CDemoFile::ReadStringTables( bf_read *buf )
{
	if ( buf )
		return ReadRawData( (char*)buf->GetBasePointer(), buf->GetNumBytesLeft() );
	return ReadRawData( NULL, 0 ); // skip data
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : cmdnumber - 
//-----------------------------------------------------------------------------
void CDemoFile::WriteUserCmd( int cmdnumber, const char *buffer, unsigned char bytes, int tick )
{
	DemoFileDbg( "WriteUserCmd()\n" );
	if ( !m_pBuffer || !m_pBuffer->IsValid() )
		return;

	WriteCmdHeader( dem_usercmd, tick );

	m_pBuffer->PutInt( cmdnumber );

	WriteRawData( buffer, bytes );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : discard - 
//-----------------------------------------------------------------------------
int CDemoFile::ReadUserCmd( char *buffer, int &size )
{
	int outgoing_sequence;
	
	Assert( m_pBuffer && m_pBuffer->IsValid() );
	outgoing_sequence = m_pBuffer->GetInt();

	size = ReadRawData( buffer, size );
	return outgoing_sequence;
}

//
// Purpose: Rewind from the current spot by the time stamp, byte code and frame counter offsets
//-----------------------------------------------------------------------------
void CDemoFile::SeekTo( int position, bool bRead )
{
	Assert( m_pBuffer && m_pBuffer->IsValid() );
	if ( bRead )
	{
		m_pBuffer->SeekGet( CUtlBuffer::SEEK_HEAD, position );
	}
	else
	{
		m_pBuffer->SeekPut( CUtlBuffer::SEEK_HEAD, position );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
int CDemoFile::ReadRawData( char *buffer, int length )
{
	int size;
	
	Assert( m_pBuffer && m_pBuffer->IsValid() );
	if ( !m_pBuffer || !m_pBuffer->IsValid() )
	{
		Host_EndGame(true, "Error reading demo message data.\n");
		return -1;
	}

	size = m_pBuffer->GetInt();

	if ( !buffer )
	{
		// just skip it
		m_pBuffer->SeekGet( CUtlBuffer::SEEK_CURRENT, size );
		return size;
	}

	if ( length < size )
	{
		// given buffer is too small
		DevMsg("CDemoFile::ReadRawData: buffer overflow (%i).\n", size );
		m_pBuffer->SeekGet( CUtlBuffer::SEEK_CURRENT, -(int)sizeof( int ) ); // rewind our get pointer
		return -1;
	}

	// read data into buffer
	m_pBuffer->Get( buffer, size );

	return size;
}

void CDemoFile::WriteRawData( const char *buffer, int length )
{
	DemoFileDbg( "WriteRawData()\n" );
	MEM_ALLOC_CREDIT();

	Assert( m_pBuffer && m_pBuffer->IsValid() );
	m_pBuffer->PutInt( length );
	m_pBuffer->Put( buffer, length );
}

void CDemoFile::WriteDemoHeader()
{
	if ( !m_bAllowHeaderWrite )
		return;

	DemoFileDbg( "WriteDemoHeader()\n" );
	Assert( m_DemoHeader.networkprotocol == PROTOCOL_VERSION );

	if ( dbg_demofile.GetInt() )
	{
		DevMsg( "\n" );
		DevMsg( "     demofilestamp: %s\n", m_DemoHeader.demofilestamp );
		DevMsg( "     demoprotocol (should be %i): %i\n", DEMO_PROTOCOL, m_DemoHeader.demoprotocol );
		DevMsg( "     networkprotocol (should be %i): %i\n", PROTOCOL_VERSION, m_DemoHeader.networkprotocol );
		DevMsg( "     servername: %s\n", m_DemoHeader.servername );
		DevMsg( "     clientname: %s\n", m_DemoHeader.clientname );
		DevMsg( "     mapname: %s\n", m_DemoHeader.mapname );
		DevMsg( "     gamedirectory: %s\n", m_DemoHeader.gamedirectory );
		DevMsg( "     playback_time: %f\n", m_DemoHeader.playback_time );
		DevMsg( "     playback_ticks: %i\n", m_DemoHeader.playback_ticks );	
		DevMsg( "     playback_frames: %i\n", m_DemoHeader.playback_frames );
		DevMsg( "     signonlength: %i\n", m_DemoHeader.signonlength );
		DevMsg( "\n" );
	}

	// Swaps endianness, goes to file start and writes header
	demoheader_t littleEndianHeader = *((demoheader_t*)&m_DemoHeader);
	ByteSwap_demoheader_t( littleEndianHeader );

	// Goto file start
	m_pBuffer->SeekPut( CUtlBuffer::SEEK_HEAD, 0 );

	// Write
	m_pBuffer->Put( &m_DemoHeader, sizeof( m_DemoHeader ) );
}

demoheader_t *CDemoFile::ReadDemoHeader()
{
	bool bOk;
	Q_memset( &m_DemoHeader, 0, sizeof(m_DemoHeader) );

	if ( !m_pBuffer || !m_pBuffer->IsValid() )
		return NULL;
	m_pBuffer->SeekGet( CUtlBuffer::SEEK_HEAD, 0 );
	m_pBuffer->Get( &m_DemoHeader, sizeof(demoheader_t) );
	bOk = m_pBuffer->IsValid();

	ByteSwap_demoheader_t( m_DemoHeader );

	if ( !bOk )
		return NULL;  // reading failed

	if ( Q_strcmp( m_DemoHeader.demofilestamp, DEMO_HEADER_ID ) )
	{
		ConMsg( "%s has invalid demo header ID.\n", m_szFileName );
		return NULL;
	}

	if ( m_DemoHeader.networkprotocol != PROTOCOL_VERSION 
#if defined( DEMO_BACKWARDCOMPATABILITY )
		&&  m_DemoHeader.networkprotocol < PROTOCOL_VERSION_12 
#endif
		)
	{
		ConMsg ("ERROR: demo network protocol %i outdated, engine version is %i \n", 
			m_DemoHeader.networkprotocol, PROTOCOL_VERSION );

		return NULL;
	}

	if ( ( m_DemoHeader.demoprotocol > DEMO_PROTOCOL) ||
		 ( m_DemoHeader.demoprotocol < 2 ) )
	{
		ConMsg ("ERROR: demo file protocol %i outdated, engine vnoteersion is %i \n", 
			m_DemoHeader.demoprotocol, DEMO_PROTOCOL );

		return NULL;
	}

	return &m_DemoHeader;
}

void CDemoFile::WriteFileBytes( FileHandle_t fh, int length )
{
	DemoFileDbg( "WriteFileBytes()\n" );
	int   copysize = length;
	char  copybuf[COM_COPY_CHUNK_SIZE];

	while ( copysize > COM_COPY_CHUNK_SIZE )
	{
		g_pFileSystem->Read ( copybuf, COM_COPY_CHUNK_SIZE, fh );
		m_pBuffer->Put( copybuf, COM_COPY_CHUNK_SIZE );
		copysize -= COM_COPY_CHUNK_SIZE;
	}

	g_pFileSystem->Read ( copybuf, copysize, fh );
	m_pBuffer->Put( copybuf, copysize );
	
	g_pFileSystem->Flush ( fh );
}

bool CDemoFile::Open(const char *name, bool bReadOnly, bool bMemoryBuffer, int nBufferSize/*=0*/, bool bAllowHeaderWrite/*=true*/)
{
	if ( m_pBuffer && m_pBuffer->IsValid() )
	{
		ConMsg ("CDemoFile::Open: file already open.\n");
		return false;
	}

	m_szFileName[0] = 0;  // clear name
	Q_memset( &m_DemoHeader, 0, sizeof(m_DemoHeader) ); // and demo header

	// This is used by replay, which manually writes a header.
	m_bAllowHeaderWrite = bAllowHeaderWrite;

	if ( bMemoryBuffer )
	{
		Assert( !bReadOnly );	// Only read from files
		Assert( nBufferSize > 0 );
		m_pBuffer = new CUtlBuffer( nBufferSize, nBufferSize, 0 );
		m_bIsStreamBuffer = false;
	}
	else
	{
		m_pBuffer = new CUtlStreamBuffer( name, NULL, bReadOnly ? CUtlBuffer::READ_ONLY : 0, false );
		m_bIsStreamBuffer = true;
	}

	// Demo files are always little endian
	m_pBuffer->SetBigEndian( false );

	if ( !m_pBuffer || !m_pBuffer->IsValid() )
	{
		ConMsg ("CDemoFile::Open: couldn't open file %s for %s.\n", 
			name, bReadOnly?"reading":"writing" );
		Close();
		return false;
	}

	if ( name )
	{
		Q_strncpy( m_szFileName, name, sizeof(m_szFileName) );
	}

	return true;
}

bool CDemoFile::IsOpen()
{
	return m_pBuffer && m_pBuffer->IsValid();
}

void CDemoFile::Close()
{
	// CUtlBuffer base class does NOT have a virtual destructor!
	if ( m_bIsStreamBuffer )
	{
		// Destructor will call Close() as needed
		delete static_cast<CUtlStreamBuffer*>(m_pBuffer);
	}
	else
	{
		delete m_pBuffer;
	}
	m_pBuffer = NULL;
}

int CDemoFile::GetSize()
{
	return m_pBuffer->TellMaxPut();
}

// Returns the PROTOCOL_VERSION used when .dem was recorded
int CDemoFile::GetProtocolVersion()
{
	return m_DemoHeader.networkprotocol;
}