/*
===========================================================================

Doom 3 BFG Edition GPL Source Code
Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company. 

This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code").  

Doom 3 BFG Edition Source Code 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 3 of the License, or
(at your option) any later version.

Doom 3 BFG Edition Source Code 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 Doom 3 BFG Edition Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 BFG Edition Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 BFG Edition Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

===========================================================================
*/
#pragma hdrstop
#include "../idlib/precompiled.h"

#include "File_SaveGame.h"

/*

TODO: CRC on each block

*/


/*
========================
ZlibAlloc
========================
*/
void * ZlibAlloc( void *opaque, uInt items, uInt size ) {
	return Mem_Alloc( items * size, TAG_SAVEGAMES );
}

/*
========================
ZlibFree
========================
*/
void ZlibFree( void *opaque, void * address ) {
	Mem_Free( address );
}

idCVar sgf_threads( "sgf_threads", "2", CVAR_INTEGER, "0 = all foreground, 1 = background write, 2 = background write + compress" );
idCVar sgf_checksums( "sgf_checksums", "1", CVAR_BOOL, "enable save game file checksums" );
idCVar sgf_testCorruption( "sgf_testCorruption", "-1", CVAR_INTEGER, "test corruption at the 128 kB compressed block" );

// this is supposed to get faster going from -15 to -9, but it gets slower as well as worse compression
idCVar sgf_windowBits( "sgf_windowBits", "-15", CVAR_INTEGER, "zlib window bits" );

bool idFile_SaveGamePipelined::cancelToTerminate = false;

class idSGFcompressThread : public idSysThread {
public:
	virtual int			Run() { sgf->CompressBlock(); return 0; }
	idFile_SaveGamePipelined * sgf;
};
class idSGFdecompressThread : public idSysThread {
public:
	virtual int			Run() { sgf->DecompressBlock(); return 0; }
	idFile_SaveGamePipelined * sgf;
};
class idSGFwriteThread : public idSysThread {
public:
	virtual int			Run() { sgf->WriteBlock(); return 0; }
	idFile_SaveGamePipelined * sgf;
};
class idSGFreadThread : public idSysThread {
public:
	virtual int			Run() { sgf->ReadBlock(); return 0; }
	idFile_SaveGamePipelined * sgf;
};

/*
============================
idFile_SaveGamePipelined::idFile_SaveGamePipelined
============================
*/
idFile_SaveGamePipelined::idFile_SaveGamePipelined() :
		mode( CLOSED ),
		compressedLength( 0 ),
		uncompressedProducedBytes( 0 ),
		uncompressedConsumedBytes( 0 ),
		compressedProducedBytes( 0 ),
		compressedConsumedBytes( 0 ),
		dataZlib( NULL ),
		bytesZlib( 0 ),
		dataIO( NULL ),
		bytesIO( 0 ),
		zLibFlushType( Z_NO_FLUSH ),
		zStreamEndHit( false ),
		numChecksums( 0 ),
		nativeFile( NULL ),
		nativeFileEndHit( false ),
		finished( false ),
		readThread( NULL ),
		writeThread( NULL ),
		decompressThread( NULL ),
		compressThread( NULL ),
		blockFinished( true ),
		buildVersion( "" ),
		saveFormatVersion( 0 ) {

	memset( &zStream, 0, sizeof( zStream ) );
	memset( compressed, 0, sizeof( compressed ) );
	memset( uncompressed, 0, sizeof( uncompressed ) );
	zStream.zalloc = ZlibAlloc;
	zStream.zfree = ZlibFree;
}

/*
============================
idFile_SaveGamePipelined::~idFile_SaveGamePipelined
============================
*/
idFile_SaveGamePipelined::~idFile_SaveGamePipelined() {
	Finish();

	// free the threads
	if ( compressThread != NULL ) {
		delete compressThread;
		compressThread = NULL;
	}
	if ( decompressThread != NULL ) {
		delete decompressThread;
		decompressThread = NULL;
	}
	if ( readThread != NULL ) {
		delete readThread;
		readThread = NULL;
	}
	if ( writeThread != NULL ) {
		delete writeThread;
		writeThread = NULL;
	}

	// close the native file
/*	if ( nativeFile != NULL ) {
		delete nativeFile;
		nativeFile = NULL;
	} */

	dataZlib = NULL;
	dataIO = NULL;
}

/*
========================
idFile_SaveGamePipelined::ReadBuildVersion
========================
*/
bool idFile_SaveGamePipelined::ReadBuildVersion() {
	return ReadString( buildVersion ) != 0;
}

/*
========================
idFile_SaveGamePipelined::ReadSaveFormatVersion
========================
*/
bool idFile_SaveGamePipelined::ReadSaveFormatVersion() {
	if ( ReadBig( pointerSize ) <= 0 ) { 
		return false; 
	}
	return ReadBig( saveFormatVersion ) != 0;
}

/*
========================
idFile_SaveGamePipelined::GetPointerSize
========================
*/
int idFile_SaveGamePipelined::GetPointerSize() const { 
	if ( pointerSize == 0 ) { 
		// in original savegames we weren't saving the pointer size, so the 2 high bytes of the save version will be 0
		return 4; 
	}  else {
		return pointerSize;
	}
}

/*
============================
idFile_SaveGamePipelined::Finish
============================
*/
void idFile_SaveGamePipelined::Finish() {
	if ( mode == WRITE ) {

		// wait for the compression thread to complete, which may kick off a write
		if ( compressThread != NULL ) {
			compressThread->WaitForThread();
		}

		// force the next compression to emit everything
		zLibFlushType = Z_FINISH;
		FlushUncompressedBlock();

		if ( compressThread != NULL ) {
			compressThread->WaitForThread();
		}

		if ( writeThread != NULL ) {
			// wait for the IO thread to exit
			writeThread->WaitForThread();
		} else if ( nativeFile == NULL && !nativeFileEndHit ) {
			// wait for the last block to be consumed
			blockRequested.Wait();
			finished = true;
			blockAvailable.Raise();
			blockFinished.Wait();
		}

		// free zlib tables
		deflateEnd( &zStream );

	} else if ( mode == READ ) {

		// wait for the decompression thread to complete, which may kick off a read
		if ( decompressThread != NULL ) {
			decompressThread->WaitForThread();
		}

		if ( readThread != NULL ) {
			// wait for the IO thread to exit
			readThread->WaitForThread();
		} else if ( nativeFile == NULL && !nativeFileEndHit ) {
			// wait for the last block to be consumed
			blockAvailable.Wait();
			finished = true;
			blockRequested.Raise();
			blockFinished.Wait();
		}

		// free zlib tables
		inflateEnd( &zStream );
	}

	mode = CLOSED;
}

/*
============================
idFile_SaveGamePipelined::Abort
============================
*/
void idFile_SaveGamePipelined::Abort() {
	if ( mode == WRITE ) {

		if ( compressThread != NULL ) {
			compressThread->WaitForThread();
		}
		if ( writeThread != NULL ) {
			writeThread->WaitForThread();
		} else if ( nativeFile == NULL && !nativeFileEndHit ) {
			blockRequested.Wait();
			finished = true;
			dataIO = NULL;
			bytesIO = 0;
			blockAvailable.Raise();
			blockFinished.Wait();
		}

	} else if ( mode == READ ) {
	
		if ( decompressThread != NULL ) {
			decompressThread->WaitForThread();
		}
		if ( readThread != NULL ) {
			readThread->WaitForThread();
		} else if ( nativeFile == NULL && !nativeFileEndHit ) {
			blockAvailable.Wait();
			finished = true;
			dataIO = NULL;
			bytesIO = 0;
			blockRequested.Raise();
			blockFinished.Wait();
		}
	}

	mode = CLOSED;
}

/*
===================================================================================

WRITE PATH

===================================================================================
*/

/*
============================
idFile_SaveGamePipelined::OpenForWriting
============================
*/
bool idFile_SaveGamePipelined::OpenForWriting( const char * const filename, bool useNativeFile ) {
	assert( mode == CLOSED );

	name = filename;
	osPath = filename;
	mode = WRITE;
	nativeFile = NULL;
	numChecksums = 0;

	if ( useNativeFile ) {
		nativeFile = fileSystem->OpenFileWrite( filename );
		if ( nativeFile == NULL ) {
			return false;
		}
	}

	// raw deflate with no header / checksum
	// use max memory for fastest compression
	// optimize for higher speed
	//mem.PushHeap();
	int status = deflateInit2( &zStream, Z_BEST_SPEED, Z_DEFLATED, sgf_windowBits.GetInteger(), 9, Z_DEFAULT_STRATEGY );
	//mem.PopHeap();
	if ( status != Z_OK ) {
		idLib::FatalError( "idFile_SaveGamePipelined::OpenForWriting: deflateInit2() error %i", status );
	}

	// initial buffer setup
	zStream.avail_out = COMPRESSED_BLOCK_SIZE;
	zStream.next_out = (Bytef * )compressed;

	if ( sgf_checksums.GetBool() ) {
		zStream.avail_out -= sizeof( uint32 );
	}

	if ( sgf_threads.GetInteger() >= 1 ) {
		compressThread = new (TAG_IDFILE) idSGFcompressThread();
		compressThread->sgf = this;
		compressThread->StartWorkerThread( "SGF_CompressThread", CORE_2B, THREAD_NORMAL );
	}
	if ( nativeFile != NULL && sgf_threads.GetInteger() >= 2 ) {
		writeThread = new (TAG_IDFILE) idSGFwriteThread();
		writeThread->sgf = this;
		writeThread->StartWorkerThread( "SGF_WriteThread", CORE_2A, THREAD_NORMAL );
	}

	return true;
}

/*
============================
idFile_SaveGamePipelined::OpenForWriting
============================
*/
bool idFile_SaveGamePipelined::OpenForWriting( idFile * file )  {
	assert( mode == CLOSED );

	if ( file == NULL ) {
		return false;
	}

	name = file->GetName();
	osPath = file->GetFullPath();
	mode = WRITE;
	nativeFile = file;
	numChecksums = 0;


	// raw deflate with no header / checksum
	// use max memory for fastest compression
	// optimize for higher speed
	//mem.PushHeap();
	int status = deflateInit2( &zStream, Z_BEST_SPEED, Z_DEFLATED, sgf_windowBits.GetInteger(), 9, Z_DEFAULT_STRATEGY );
	//mem.PopHeap();
	if ( status != Z_OK ) {
		idLib::FatalError( "idFile_SaveGamePipelined::OpenForWriting: deflateInit2() error %i", status );
	}

	// initial buffer setup
	zStream.avail_out = COMPRESSED_BLOCK_SIZE;
	zStream.next_out = (Bytef * )compressed;

	if ( sgf_checksums.GetBool() ) {
		zStream.avail_out -= sizeof( uint32 );
	}

	if ( sgf_threads.GetInteger() >= 1 ) {
		compressThread = new (TAG_IDFILE) idSGFcompressThread();
		compressThread->sgf = this;
		compressThread->StartWorkerThread( "SGF_CompressThread", CORE_2B, THREAD_NORMAL );
	}
	if ( nativeFile != NULL && sgf_threads.GetInteger() >= 2 ) {
		writeThread = new (TAG_IDFILE) idSGFwriteThread();
		writeThread->sgf = this;
		writeThread->StartWorkerThread( "SGF_WriteThread", CORE_2A, THREAD_NORMAL );
	}

	return true;
}

/*
============================
idFile_SaveGamePipelined::NextWriteBlock

Modifies:
	dataIO
	bytesIO
============================
*/
bool idFile_SaveGamePipelined::NextWriteBlock( blockForIO_t * block ) {
	assert( mode == WRITE );

	blockRequested.Raise();		// the background thread is done with the last block

	if ( nativeFileEndHit ) {
		return false;
	}

	blockAvailable.Wait();	// wait for a new block to come through the pipeline

	if ( finished || block == NULL ) {
		nativeFileEndHit = true;
		blockRequested.Raise();
		blockFinished.Raise();
		return false;
	}

	compressedLength += bytesIO;

	block->data = dataIO;
	block->bytes = bytesIO;

	dataIO = NULL;
	bytesIO = 0;

	return true;
}

/*
============================
idFile_SaveGamePipelined::WriteBlock

Modifies:
	dataIO
	bytesIO
	nativeFile
============================
*/
void idFile_SaveGamePipelined::WriteBlock() {
	assert( nativeFile != NULL );

	compressedLength += bytesIO;

	nativeFile->Write( dataIO, bytesIO );

	dataIO = NULL;
	bytesIO = 0;
}

/*
============================
idFile_SaveGamePipelined::FlushCompressedBlock

Called when a compressed block fills up, and also to flush the final partial block.
Flushes everything from [compressedConsumedBytes -> compressedProducedBytes)

Reads:
	compressed
	compressedProducedBytes

Modifies:
	dataZlib
	bytesZlib
	compressedConsumedBytes
============================
*/
void idFile_SaveGamePipelined::FlushCompressedBlock() {
	// block until the background thread is done with the last block
	if ( writeThread != NULL ) {
		writeThread->WaitForThread();
	} if ( nativeFile == NULL ) {
		if ( !nativeFileEndHit ) {
			blockRequested.Wait();
		}
	}

	// prepare the next block to be written out
	dataIO = &compressed[ compressedConsumedBytes & ( COMPRESSED_BUFFER_SIZE - 1 ) ];
	bytesIO = compressedProducedBytes - compressedConsumedBytes;
	compressedConsumedBytes = compressedProducedBytes;

	if ( writeThread != NULL ) {
		// signal a new block is available to be written out
		writeThread->SignalWork();
	} else if ( nativeFile != NULL ) {
		// write syncronously
		WriteBlock();
	} else {
		// signal a new block is available to be written out
		blockAvailable.Raise();
	}
}

/*
============================
idFile_SaveGamePipelined::CompressBlock

Called when an uncompressed block fills up, and also to flush the final partial block.
Flushes everything from [uncompressedConsumedBytes -> uncompressedProducedBytes)

Modifies:
	dataZlib
	bytesZlib
	compressed
	compressedProducedBytes
	zStream
	zStreamEndHit
============================
*/
void idFile_SaveGamePipelined::CompressBlock() {
	zStream.next_in = (Bytef * )dataZlib;
	zStream.avail_in = (uInt) bytesZlib;

	dataZlib = NULL;
	bytesZlib = 0;

	// if this is the finish block, we may need to write
	// multiple buffers even after all input has been consumed
	while( zStream.avail_in > 0 || zLibFlushType == Z_FINISH ) {

		const int zstat = deflate( &zStream, zLibFlushType );

		if ( zstat != Z_OK && zstat != Z_STREAM_END ) {
			idLib::FatalError( "idFile_SaveGamePipelined::CompressBlock: deflate() returned %i", zstat );
		}

		if ( zStream.avail_out == 0 || zLibFlushType == Z_FINISH ) {

			if ( sgf_checksums.GetBool() ) {
				size_t blockSize = zStream.total_out + numChecksums * sizeof( uint32 ) - compressedProducedBytes;
				uint32 checksum = MD5_BlockChecksum( zStream.next_out - blockSize, blockSize );
				zStream.next_out[0] = ( ( checksum >>  0 ) & 0xFF );
				zStream.next_out[1] = ( ( checksum >>  8 ) & 0xFF );
				zStream.next_out[2] = ( ( checksum >> 16 ) & 0xFF );
				zStream.next_out[3] = ( ( checksum >> 24 ) & 0xFF );
				numChecksums++;
			}

			// flush the output buffer IO
			compressedProducedBytes = zStream.total_out + numChecksums * sizeof( uint32 );
			FlushCompressedBlock();
			if ( zstat == Z_STREAM_END ) {
				assert( zLibFlushType == Z_FINISH );
				zStreamEndHit = true;
				return;
			}

			assert( 0 == ( compressedProducedBytes & ( COMPRESSED_BLOCK_SIZE - 1 ) ) );

			zStream.avail_out = COMPRESSED_BLOCK_SIZE;
			zStream.next_out = (Bytef * )&compressed[ compressedProducedBytes & ( COMPRESSED_BUFFER_SIZE - 1 ) ];

			if ( sgf_checksums.GetBool() ) {
				zStream.avail_out -= sizeof( uint32 );
			}
		}
	}
}

/*
============================
idFile_SaveGamePipelined::FlushUncompressedBlock

Called when an uncompressed block fills up, and also to flush the final partial block.
Flushes everything from [uncompressedConsumedBytes -> uncompressedProducedBytes)

Reads:
	uncompressed
	uncompressedProducedBytes

Modifies:
	dataZlib
	bytesZlib
	uncompressedConsumedBytes
============================
*/
void idFile_SaveGamePipelined::FlushUncompressedBlock() {
	// block until the background thread has completed
	if ( compressThread != NULL ) {
		// make sure thread has completed the last work
		compressThread->WaitForThread();
	}

	// prepare the next block to be consumed by Zlib
	dataZlib = &uncompressed[ uncompressedConsumedBytes & ( UNCOMPRESSED_BUFFER_SIZE - 1 ) ];
	bytesZlib = uncompressedProducedBytes - uncompressedConsumedBytes;
	uncompressedConsumedBytes = uncompressedProducedBytes;

	if ( compressThread != NULL ) {
		// signal thread for more work
		compressThread->SignalWork();
	} else {
		// run syncronously
		CompressBlock();
	}
}

/*
============================
idFile_SaveGamePipelined::Write

Modifies:
	uncompressed
	uncompressedProducedBytes
============================
*/
int idFile_SaveGamePipelined::Write( const void * buffer, int length ) {
	if ( buffer == NULL || length <= 0 ) {
		return 0;
	}

#if 1	// quick and dirty fix for user-initiated forced shutdown during a savegame
	if ( cancelToTerminate ) {
		if ( mode != CLOSED ) {
			Abort();
		}
		return 0;
	}
#endif

	assert( mode == WRITE );
	size_t lengthRemaining = length;
	const byte * buffer_p = (const byte *)buffer;
	while ( lengthRemaining > 0 ) {
		const size_t ofsInBuffer = uncompressedProducedBytes & ( UNCOMPRESSED_BUFFER_SIZE - 1 );
		const size_t ofsInBlock = uncompressedProducedBytes & ( UNCOMPRESSED_BLOCK_SIZE - 1 );
		const size_t remainingInBlock = UNCOMPRESSED_BLOCK_SIZE - ofsInBlock;
		const size_t copyToBlock = ( lengthRemaining < remainingInBlock ) ? lengthRemaining : remainingInBlock;

		memcpy( uncompressed + ofsInBuffer, buffer_p, copyToBlock );
		uncompressedProducedBytes += copyToBlock;

		buffer_p += copyToBlock;
		lengthRemaining -= copyToBlock;

		if ( copyToBlock == remainingInBlock ) {
			FlushUncompressedBlock();
		}
	}
	return length;
}

/*
===================================================================================

READ PATH

===================================================================================
*/

/*
============================
idFile_SaveGamePipelined::OpenForReading
============================
*/
bool idFile_SaveGamePipelined::OpenForReading( const char * const filename, bool useNativeFile ) {
	assert( mode == CLOSED );

	name = filename;
	osPath = filename;
	mode = READ;
	nativeFile = NULL;
	numChecksums = 0;

	if ( useNativeFile ) {
		nativeFile = fileSystem->OpenFileRead( filename );
		if ( nativeFile == NULL ) {
			return false;
		}
	}

	// init zlib for raw inflate with a 32k dictionary
	//mem.PushHeap();
	int status = inflateInit2( &zStream, sgf_windowBits.GetInteger() );
	//mem.PopHeap();
	if ( status != Z_OK ) {
		idLib::FatalError( "idFile_SaveGamePipelined::OpenForReading: inflateInit2() error %i", status );
	}

	// spawn threads
	if ( sgf_threads.GetInteger() >= 1 ) {
		decompressThread = new (TAG_IDFILE) idSGFdecompressThread();
		decompressThread->sgf = this;
		decompressThread->StartWorkerThread( "SGF_DecompressThread", CORE_2B, THREAD_NORMAL );
	}
	if ( nativeFile != NULL && sgf_threads.GetInteger() >= 2 ) {
		readThread = new (TAG_IDFILE) idSGFreadThread();
		readThread->sgf = this;
		readThread->StartWorkerThread( "SGF_ReadThread", CORE_2A, THREAD_NORMAL );
	}

	return true;
}


/*
============================
idFile_SaveGamePipelined::OpenForReading
============================
*/
bool idFile_SaveGamePipelined::OpenForReading( idFile * file ) {
	assert( mode == CLOSED );

	if ( file == NULL ) {
		return false;
	}

	name = file->GetName();
	osPath = file->GetFullPath();
	mode = READ;
	nativeFile = file;
	numChecksums = 0;

	// init zlib for raw inflate with a 32k dictionary
	//mem.PushHeap();
	int status = inflateInit2( &zStream, sgf_windowBits.GetInteger() );
	//mem.PopHeap();
	if ( status != Z_OK ) {
		idLib::FatalError( "idFile_SaveGamePipelined::OpenForReading: inflateInit2() error %i", status );
	}

	// spawn threads
	if ( sgf_threads.GetInteger() >= 1 ) {
		decompressThread = new (TAG_IDFILE) idSGFdecompressThread();
		decompressThread->sgf = this;
		decompressThread->StartWorkerThread( "SGF_DecompressThread", CORE_1B, THREAD_NORMAL );
	}
	if ( nativeFile != NULL && sgf_threads.GetInteger() >= 2 ) {
		readThread = new (TAG_IDFILE) idSGFreadThread();
		readThread->sgf = this;
		readThread->StartWorkerThread( "SGF_ReadThread", CORE_1A, THREAD_NORMAL );
	}

	return true;
}


/*
============================
idFile_SaveGamePipelined::NextReadBlock

Reads the next data block from the filesystem into the memory buffer.

Modifies:
	compressed
	compressedProducedBytes
	nativeFileEndHit
============================
*/
bool idFile_SaveGamePipelined::NextReadBlock( blockForIO_t * block, size_t lastReadBytes ) {
	assert( mode == READ );

	assert( ( lastReadBytes & ( COMPRESSED_BLOCK_SIZE - 1 ) ) == 0 || block == NULL );
	compressedProducedBytes += lastReadBytes;

	blockAvailable.Raise();		// a new block is available for the pipeline to consume

	if ( nativeFileEndHit ) {
		return false;
	}

	blockRequested.Wait();		// wait for the last block to be consumed by the pipeline

	if ( finished || block == NULL ) {
		nativeFileEndHit = true;
		blockAvailable.Raise();
		blockFinished.Raise();
		return false;
	}

	assert( 0 == ( compressedProducedBytes & ( COMPRESSED_BLOCK_SIZE - 1 ) ) );
	block->data = & compressed[compressedProducedBytes & ( COMPRESSED_BUFFER_SIZE - 1 )];
	block->bytes = COMPRESSED_BLOCK_SIZE;

	return true;
}

/*
============================
idFile_SaveGamePipelined::ReadBlock

Reads the next data block from the filesystem into the memory buffer.

Modifies:
	compressed
	compressedProducedBytes
	nativeFile
	nativeFileEndHit
============================
*/
void idFile_SaveGamePipelined::ReadBlock() {
	assert( nativeFile != NULL );
	// normally run in a separate thread
	if ( nativeFileEndHit ) {
		return;
	}
	// when we are reading the last block of the file, we may not fill the entire block
	assert( 0 == ( compressedProducedBytes & ( COMPRESSED_BLOCK_SIZE - 1 ) ) );
	byte * dest = &compressed[ compressedProducedBytes & ( COMPRESSED_BUFFER_SIZE - 1 ) ];
	size_t ioBytes = nativeFile->Read( dest, COMPRESSED_BLOCK_SIZE );
	compressedProducedBytes += ioBytes;
	if ( ioBytes != COMPRESSED_BLOCK_SIZE ) {
		nativeFileEndHit = true;
	}
}

/*
============================
idFile_SaveGamePipelined::PumpCompressedBlock

Reads:
	compressed
	compressedProducedBytes

Modifies:
	dataIO
	byteIO
	compressedConsumedBytes
============================
*/
void idFile_SaveGamePipelined::PumpCompressedBlock() {
	// block until the background thread is done with the last block
	if ( readThread != NULL ) {
		readThread->WaitForThread();
	} else if ( nativeFile == NULL ) {
		if ( !nativeFileEndHit ) {
			blockAvailable.Wait();
		}
	}

	// fetch the next block read in
	dataIO = &compressed[ compressedConsumedBytes & ( COMPRESSED_BUFFER_SIZE - 1 ) ];
	bytesIO = compressedProducedBytes - compressedConsumedBytes;
	compressedConsumedBytes = compressedProducedBytes;

	if ( readThread != NULL ) {
		// signal read thread to read another block
		readThread->SignalWork();
	} else if ( nativeFile != NULL ) {
		// run syncronously
		ReadBlock();
	} else {
		// request a new block
		blockRequested.Raise();
	}
}

/*
============================
idFile_SaveGamePipelined::DecompressBlock

Decompresses the next data block from the memory buffer

Normally this runs in a separate thread when signalled, but
can be called in the main thread for debugging.

This will not exit until a complete block has been decompressed,
unless end-of-file is reached.

This may require additional compressed blocks to be read.

Reads:
	nativeFileEndHit

Modifies:
	dataIO
	bytesIO
	uncompressed
	uncompressedProducedBytes
	zStreamEndHit
	zStream
============================
*/
void idFile_SaveGamePipelined::DecompressBlock() {
	if ( zStreamEndHit ) {
		return;
	}

	assert( ( uncompressedProducedBytes & ( UNCOMPRESSED_BLOCK_SIZE - 1 ) ) == 0 );
	zStream.next_out = (Bytef * )&uncompressed[ uncompressedProducedBytes & ( UNCOMPRESSED_BUFFER_SIZE - 1 ) ];
	zStream.avail_out = UNCOMPRESSED_BLOCK_SIZE;

	while( zStream.avail_out > 0 ) {
		if ( zStream.avail_in == 0 ) {
			do {
				PumpCompressedBlock();
				if ( bytesIO == 0 && nativeFileEndHit ) {
					// don't try to decompress any more if there is no more data
					zStreamEndHit = true;
					return;
				}
			} while ( bytesIO == 0 );

			zStream.next_in = (Bytef *) dataIO;
			zStream.avail_in = (uInt) bytesIO;

			dataIO = NULL;
			bytesIO = 0;

			if ( sgf_checksums.GetBool() ) {
				if ( sgf_testCorruption.GetInteger() == numChecksums ) {
					zStream.next_in[0] ^= 0xFF;
				}
				zStream.avail_in -= sizeof( uint32 );
				uint32 checksum = MD5_BlockChecksum( zStream.next_in, zStream.avail_in );
				if (	!verify( zStream.next_in[zStream.avail_in + 0] == ( ( checksum >>  0 ) & 0xFF ) ) ||
						!verify( zStream.next_in[zStream.avail_in + 1] == ( ( checksum >>  8 ) & 0xFF ) ) ||
						!verify( zStream.next_in[zStream.avail_in + 2] == ( ( checksum >> 16 ) & 0xFF ) ) ||
						!verify( zStream.next_in[zStream.avail_in + 3] == ( ( checksum >> 24 ) & 0xFF ) ) ) {
					// don't try to decompress any more if the checksum is wrong
					zStreamEndHit = true;
					return;
				}
				numChecksums++;
			}
		}

		const int zstat = inflate( &zStream, Z_SYNC_FLUSH );

		uncompressedProducedBytes = zStream.total_out;

		if ( zstat == Z_STREAM_END ) {
			// don't try to decompress any more
			zStreamEndHit = true;
			return;
		}
		if ( zstat != Z_OK ) {
			idLib::Warning( "idFile_SaveGamePipelined::DecompressBlock: inflate() returned %i", zstat );
			zStreamEndHit = true;
			return;
		}
	}

	assert( ( uncompressedProducedBytes & ( UNCOMPRESSED_BLOCK_SIZE - 1 ) ) == 0 );
}

/*
============================
idFile_SaveGamePipelined::PumpUncompressedBlock

Called when an uncompressed block is drained.

Reads:
	uncompressed
	uncompressedProducedBytes

Modifies:
	dataZlib
	bytesZlib
	uncompressedConsumedBytes
============================
*/
void idFile_SaveGamePipelined::PumpUncompressedBlock() {
	if ( decompressThread != NULL ) {
		// make sure thread has completed the last work
		decompressThread->WaitForThread();
	}

	// fetch the next block produced by Zlib
	dataZlib = &uncompressed[ uncompressedConsumedBytes & ( UNCOMPRESSED_BUFFER_SIZE - 1 ) ];
	bytesZlib = uncompressedProducedBytes - uncompressedConsumedBytes;
	uncompressedConsumedBytes = uncompressedProducedBytes;

	if ( decompressThread != NULL ) {
		// signal thread for more work
		decompressThread->SignalWork();
	} else {
		// run syncronously
		DecompressBlock();
	}
}

/*
============================
idFile_SaveGamePipelined::Read

Modifies:
	dataZlib
	bytesZlib
============================
*/
int idFile_SaveGamePipelined::Read( void * buffer, int length ) {
	if ( buffer == NULL || length <= 0 ) {
		return 0;
	}

	assert( mode == READ );

	size_t ioCount = 0;
	size_t lengthRemaining = length;
	byte * buffer_p = (byte *)buffer;
	while ( lengthRemaining > 0 ) {
		while ( bytesZlib == 0 ) {
			PumpUncompressedBlock();
			if ( bytesZlib == 0 && zStreamEndHit ) {
				return ioCount;
			}
		}

		const size_t copyFromBlock = ( lengthRemaining < bytesZlib ) ? lengthRemaining : bytesZlib;

		memcpy( buffer_p, dataZlib, copyFromBlock );
		dataZlib += copyFromBlock;
		bytesZlib -= copyFromBlock;

		buffer_p += copyFromBlock;
		ioCount += copyFromBlock;
		lengthRemaining -= copyFromBlock;
	}
	return ioCount;
}

/*
===================================================================================

TEST CODE

===================================================================================
*/

/*
============================
TestProcessFile
============================
*/
static void TestProcessFile( const char * const filename ) {
	idLib::Printf( "Processing %s:\n", filename );
	// load some test data
	void *testData;
	const int testDataLength = fileSystem->ReadFile( filename, &testData, NULL );

	const char * const outFileName = "junk/savegameTest.bin";
	idFile_SaveGamePipelined *saveFile = new (TAG_IDFILE) idFile_SaveGamePipelined;
	saveFile->OpenForWriting( outFileName, true );

	const uint64 startWriteMicroseconds = Sys_Microseconds();

	saveFile->Write( testData, testDataLength );
	delete saveFile;		// final flush
	const int readDataLength = fileSystem->GetFileLength( outFileName );

	const uint64 endWriteMicroseconds = Sys_Microseconds();
	const uint64 writeMicroseconds = endWriteMicroseconds - startWriteMicroseconds;

	idLib::Printf( "%lld microseconds to compress %i bytes to %i written bytes = %4.1f MB/s\n", 
		writeMicroseconds, testDataLength, readDataLength, (float)readDataLength / writeMicroseconds );

	void * readData = (void *)Mem_Alloc( testDataLength, TAG_SAVEGAMES );

	const uint64 startReadMicroseconds = Sys_Microseconds();

	idFile_SaveGamePipelined *loadFile = new (TAG_IDFILE) idFile_SaveGamePipelined;
	loadFile->OpenForReading( outFileName, true );
	loadFile->Read( readData, testDataLength );
	delete loadFile;

	const uint64 endReadMicroseconds = Sys_Microseconds();
	const uint64 readMicroseconds = endReadMicroseconds - startReadMicroseconds;

	idLib::Printf( "%lld microseconds to decompress = %4.1f MB/s\n", readMicroseconds, (float)testDataLength / readMicroseconds );

	int comparePoint;
	for ( comparePoint = 0; comparePoint < testDataLength; comparePoint++ ) {
		if ( ((byte *)readData)[comparePoint] != ((byte *)testData)[comparePoint] ) {
			break;
		}
	}
	if ( comparePoint != testDataLength ) {
		idLib::Printf( "Compare failed at %i.\n", comparePoint );
		assert( 0 );
	} else {
		idLib::Printf( "Compare succeeded.\n" );
	}
	Mem_Free( readData );
	Mem_Free( testData );
}

/*
============================
TestSaveGameFile
============================
*/
CONSOLE_COMMAND( TestSaveGameFile, "Exercises the pipelined savegame code", 0 ) {
#if 1
	TestProcessFile( "maps/game/wasteland1/wasteland1.map" );
#else
	// test every file in base (found a fencepost error >100 files in originally!)
	idFileList * fileList = fileSystem->ListFiles( "", "" );
	for ( int i = 0; i < fileList->GetNumFiles(); i++ ) {
		TestProcessFile( fileList->GetFile( i ) );
		common->UpdateConsoleDisplay();
	}
	delete fileList;
#endif
}

/*
============================
TestCompressionSpeeds
============================
*/
CONSOLE_COMMAND( TestCompressionSpeeds, "Compares zlib and our code", 0 ) {
	const char * const filename = "-colorMap.tga";

	idLib::Printf( "Processing %s:\n", filename );
	// load some test data
	void *testData;
	const int testDataLength = fileSystem->ReadFile( filename, &testData, NULL );

	const int startWriteMicroseconds = Sys_Microseconds();

	idCompressor *compressor = idCompressor::AllocLZW();
//	idFile *f = fileSystem->OpenFileWrite( "junk/lzwTest.bin" );
	idFile_Memory *f = new (TAG_IDFILE) idFile_Memory( "junk/lzwTest.bin" );
	compressor->Init( f, true, 8 );

	compressor->Write( testData, testDataLength );

	const int readDataLength = f->Tell();

	delete compressor;
	delete f;

	const int endWriteMicroseconds = Sys_Microseconds();
	const int writeMicroseconds = endWriteMicroseconds - startWriteMicroseconds;

	idLib::Printf( "%i microseconds to compress %i bytes to %i written bytes = %4.1f MB/s\n", 
		writeMicroseconds, testDataLength, readDataLength, (float)readDataLength / writeMicroseconds );

}

