/*****************************************
 *
 * GOB File System
 *
 * Here's what Merriam-Webster says about "gob":  --Chuck
 *		Entry:     gob
 *		Function:  noun
 *		Etymology: Middle English gobbe, from Middle French gobe large piece of food,
 *		           back-formation from gobet
 *		Date:      14th century
 *		1 : LUMP
 *		2 : a large amount -- usually used in plural <gobs of money>
 *
 * Purpose: Provide fast, efficient disk access on a variety of platforms.
 *
 * Implementation:
 *		The GOB system maintains two files -- GOB and GFC.  The GOB file is actually
 *		an archive of many files split into variable size, compressed blocks.  The GFC,
 *		GOB File Control, contains 3 tables -- a block table, basic file table, and
 *		extended file table.  The block table is analogous to a DOS FAT.  The basic
 *		file table contains a minimal set of file information to handle basic reading
 *		tasks.  The extended file table is optionally loaded and contains additional
 *		file information.  File names are case insensitive.
 *		  
 *		Files can be read in a normal manner.  Open, read, seek and close
 *		operations are all provided.  Files can only be written in a single
 *		contiguous chunk of blocks at the end of an archive.  Reads are processed
 *		through a configurable number of read ahead buffers to in an effort to
 *		minimize both reads and seeks.  Other operations including delete, verify, 
 *		access, and get size are also supported on files inside an archive.
 *
 *		The system supports read profiling.  By supplying a file read callback 
 *		function, the library will output the block number of each read.  This can 
 *		be used rearrange block in the archive to minimize seek times.  The 
 *		GOBRearrange sorts files in an archive.
 *
 *		Supports block based caching.  Primarily aimed at caching files off a DVD/CD
 *		to a faster hard disk.
 *
 * Future Work: 
 *
 * Dependencies: vvInt, snprintf, zlib
 * Owner: Chris McEvoy
 * History:
 *     09/23/2001 Original version
 *     10/28/2002 Merged into vvtech
 *
 * Copyright (C) 2002, Vicarious Visions, Inc.  All Rights Reserved.
 *
 * UNPUBLISHED -- Rights  reserved  under  the  copyright  laws  of the 
 * United States.  Use  of a copyright notice is precautionary only and 
 * does not imply publication or disclosure.                            
 *                                                                      
 * THIS DOCUMENTATION CONTAINS CONFIDENTIAL AND PROPRIETARY INFORMATION 
 * OF    VICARIOUS   VISIONS,  INC.    ANY  DUPLICATION,  MODIFICATION, 
 * DISTRIBUTION, OR DISCLOSURE IS STRICTLY PROHIBITED WITHOUT THE PRIOR 
 * EXPRESS WRITTEN PERMISSION OF VICARIOUS VISIONS, INC.
 *
 *****************************************/

/*
	This is an unofficial branch of GOB, for Jedi Academy
	Maintainer: Brian Osman
*/

#include "goblib.h"
#include "../zlib/zlib.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>

#if (VV_PLATFORM == VV_PLATFORM_WIN) || (VV_PLATFORM == VV_PLATFORM_XBOX)
#	define CDECL __cdecl
#else
#	define CDECL 
#endif

// Profiling data
static GOBProfileReadFunc ProfileReadCallback = NULL;
static GOBBool ProfileEnabled = GOB_FALSE;

// Indicates whether or not the library has been initialized
static GOBBool LibraryInit = GOB_FALSE;

// Callbacks for handling low-level compression/decompression
static struct GOBCodecFuncSet CodecFuncs;

// Callbacks for handling low-level memory alloc and free
static struct GOBMemoryFuncSet MemFuncs;

// Callbacks for handling low-level file access
static struct GOBFileSysFuncSet FSFuncs;

// Callbacks for handling block caching (ie Xbox temp space)
static struct GOBCacheFileFuncSet CacheFileFuncs;
static GOBBool CacheFileActive = GOB_FALSE;

// Name of the GFC file
static GOBChar ControlFileName[GOB_MAX_FILE_NAME_LEN];

// Handle to the GOB archive
static GOBFSHandle ArchiveHandle = (GOBFSHandle*)0xFFFFFFFF;

// Size of the active GOB archive
static GOBUInt32 ArchiveSize = 0;
static GOBUInt32 ArchiveNumBlocks = 0;
static GOBUInt32 ArchiveNumFiles = 0;

// Cached blocks
struct GOBBlockCache
{
	GOBChar* data;
	GOBUInt32 block;
	GOBUInt32 time;
	GOBUInt32 size;
};
static struct GOBBlockCache* CacheBlocks = NULL;
static GOBUInt32 NumCacheBlocks = 0;
static GOBUInt32 CacheBlockCounter = 0;

// Read ahead buffer
struct GOBReadBuffer
{
	GOBChar* data;
	GOBChar* dataStart;
	GOBUInt32 pos;
	GOBUInt32 size;
};
static struct GOBReadBuffer ReadBuffer;

// Decompression buffer
static GOBChar* DecompBuffer = NULL;

// Stats gathering
static struct GOBReadStats ReadStats;
static GOBUInt32 CurrentArchivePos = 0;

// File tables (from the GFC)
static struct GOBFileTableBasicEntry* FileTableBasic = NULL;
static struct GOBFileTableExtEntry* FileTableExt = NULL;

// Block tables (from the GFC)
static struct GOBBlockTableEntry* BlockTable = NULL;
static GOBUInt32* BlockCRC = NULL;
static GOBUInt32* CacheFileTable = NULL;

// Do the tables need to be written?
static GOBBool FileTableDirty = GOB_FALSE;

// Information about open files
struct OpenFileInfo
{
	GOBBool valid;
	GOBUInt32 startBlock;
	GOBUInt32 block;
	GOBUInt32 offset;

	GOBUInt32 pos;
	GOBUInt32 size;
};

// Open file table -- indices in this array are passed
// back to the caller as pseudo file handles.
static struct OpenFileInfo OpenFiles[GOB_MAX_OPEN_FILES];

// Converting text to lower case -- this isn't very
// clean.  A common buffer is used to store lower case
// text.  So its not thread safe... among other things. ;)
static GOBChar LowerCaseBuffer[GOB_MAX_FILE_NAME_LEN];
static GOBChar* LowerCase(const GOBChar* name)
{
	GOBInt32 i;
	for (i = 0; name[i]; ++i) {
		LowerCaseBuffer[i] = (GOBChar)tolower(name[i]);
	}
	LowerCaseBuffer[i] = 0;

	return LowerCaseBuffer;
}

// Checks if a file handle is invalid
static GOBBool InvalidHandle(GOBFSHandle h)
{
	return (GOBUInt32)h == 0xFFFFFFFF ? GOB_TRUE : GOB_FALSE;
}

// Endian conversion
#if VV_ENDIAN == VV_ENDIAN_LITTLE
static GOBUInt32 SwapBytes(GOBUInt32 x)
{
	return
		(x >> 24) |
		((x >> 8) & 0xFF00) |
		((x << 8) & 0xFF0000) |
		(x << 24);

}
#else
static GOBUInt32 SwapBytes(GOBUInt32 x)
{
	return x;
}
#endif


// Given a file name, get its index in the FileTable
static GOBInt32 GetFileTableEntry(const GOBChar* file)
{
	GOBUInt32 entry;
	GOBUInt32 hash;
	
	// hash the file name
	hash = crc32(0L, Z_NULL, 0);
	hash = crc32(hash, (const unsigned char*)file, strlen(file));

	// linear search for matching a matching hash
	for (entry = 0; entry < ArchiveNumFiles; ++entry) {
		if (FileTableBasic[entry].block != GOB_INVALID_BLOCK &&
			FileTableBasic[entry].hash == hash) 
		{
			return entry;
		}
	}

	return -1;
}

// Mark the contents of cache and read buffer invalid
static GOBVoid InvalidateCache(GOBVoid)
{
	GOBUInt32 i;
	for (i = 0; i < NumCacheBlocks; ++i) {
		CacheBlocks[i].block = 0xFFFFFFFF;
	}
	ReadBuffer.pos = 0xFFFFFFFF;
}

// Deallocate memory used by cache and read buffer
static GOBVoid FreeCache(GOBVoid)
{
	GOBUInt32 i;

	if (CacheBlocks) {
		for (i = 0; i < NumCacheBlocks; ++i) {
			if (CacheBlocks[i].data) MemFuncs.free(CacheBlocks[i].data);
			CacheBlocks[i].data = NULL;
		}

		MemFuncs.free(CacheBlocks);
		NumCacheBlocks = 0;
		CacheBlocks = NULL;
	}
}

// Write the file table to disk if the form of a GFC
static GOBError CommitFileTable(GOBVoid)
{
	GOBUInt32 num;
	struct GOBFileTableBasicEntry basic;
	struct GOBFileTableExtEntry ext;
	struct GOBBlockTableEntry block;
	
	// open the GFC
	GOBFSHandle handle = FSFuncs.open(ControlFileName, GOBACCESS_WRITE);
	if (InvalidHandle(handle)) return GOBERR_FILE_WRITE;

	// write the magic identifier
	num = SwapBytes(GOB_MAGIC_IDENTIFIER);
	if (!FSFuncs.write(handle, &num, sizeof(num))) return GOBERR_FILE_WRITE;

	// write the size of the GOB
	num = SwapBytes(ArchiveSize);
	if (!FSFuncs.write(handle, &num, sizeof(num))) return GOBERR_FILE_WRITE;

	// write number of blocks in archive
	num = SwapBytes(ArchiveNumBlocks);
	if (!FSFuncs.write(handle, &num, sizeof(num))) return GOBERR_FILE_WRITE;

	// write number of file in archive
	num = SwapBytes(ArchiveNumFiles);
	if (!FSFuncs.write(handle, &num, sizeof(num))) return GOBERR_FILE_WRITE;

	// write block table -- with endian conversion
	for (num = 0; num < ArchiveNumBlocks; ++num) {
		block.next = SwapBytes(BlockTable[num].next);
		block.offset = SwapBytes(BlockTable[num].offset);
		block.size = SwapBytes(BlockTable[num].size);

		if (!FSFuncs.write(handle, &block, sizeof(block))) return GOBERR_FILE_WRITE;
	}
	
	// write block CRCs -- with endian conversion
	for (num = 0; num < ArchiveNumBlocks; ++num) {
		BlockCRC[num] = SwapBytes(BlockCRC[num]);
		if (!FSFuncs.write(handle, &BlockCRC[num], sizeof(BlockCRC[num]))) {
			return GOBERR_FILE_WRITE;
		}
	}
	
	// write each basic table entry -- with endian conversion
	for (num = 0; num < ArchiveNumFiles; ++num) {
		basic.hash = SwapBytes(FileTableBasic[num].hash);
		basic.block = SwapBytes(FileTableBasic[num].block);
		basic.size = SwapBytes(FileTableBasic[num].size);

		if (!FSFuncs.write(handle, &basic, sizeof(basic))) return GOBERR_FILE_WRITE;
	}
	
	// write each extended table entry -- with endian conversion
	for (num = 0; num < ArchiveNumFiles; ++num) {
		strcpy(ext.name, FileTableExt[num].name);
		ext.crc = SwapBytes(FileTableExt[num].crc);
		ext.time = SwapBytes(FileTableExt[num].time);

		if (!FSFuncs.write(handle, &ext, sizeof(ext))) return GOBERR_FILE_WRITE;
	}

	// all done
	FSFuncs.close(&handle);
	FileTableDirty = GOB_FALSE;

	return GOBERR_OK;
}


static GOBVoid DeallocTables(GOBVoid)
{
	if (BlockTable) {
		// free the block table
		MemFuncs.free(BlockTable);
		BlockTable = NULL;
	}

	if (BlockCRC) {
		// free the block crc table
		MemFuncs.free(BlockCRC);
		BlockCRC = NULL;
	}

	if (CacheFileTable)
	{
		// free the block cache table
		MemFuncs.free(CacheFileTable);
		CacheFileTable = NULL;
	}

	if (FileTableBasic) {
		// free the basic file table
		MemFuncs.free(FileTableBasic);
		FileTableBasic = NULL;
	}

	if (FileTableExt) {
		// free the extended file table
		MemFuncs.free(FileTableExt);
		FileTableExt = NULL;
	}
}

static GOBError AllocTables(GOBUInt32 num_blocks, GOBUInt32 num_files, 
	GOBBool extended, GOBBool safe)
{
	GOBUInt32 num;

	// dump any old tables
	DeallocTables();
	
	// allocate the block table
	BlockTable = (struct GOBBlockTableEntry*)
		MemFuncs.alloc(num_blocks * sizeof(struct GOBBlockTableEntry));
	if (!BlockTable) return GOBERR_NO_MEMORY;

	if (safe) {
		// allocate the block crc table for verifying data validity
		BlockCRC = (GOBUInt32*)MemFuncs.alloc(num_blocks * sizeof(GOBUInt32));
		if (!BlockCRC) return GOBERR_NO_MEMORY;
	}
	else {
		BlockCRC = NULL;
	}

	if (CacheFileActive)
	{
		// allocate the block cache bitfield
		CacheFileTable = (GOBUInt32*)
			MemFuncs.alloc((num_blocks / 32 + 1) * 4);
		if (!CacheFileTable) return GOBERR_NO_MEMORY;
	}

	// allocate the basic file table
	FileTableBasic = (struct GOBFileTableBasicEntry*)
		MemFuncs.alloc(num_files * sizeof(struct GOBFileTableBasicEntry));
	if (!FileTableBasic) return GOBERR_NO_MEMORY;

	if (extended) {
		// allocate the extended file table
		FileTableExt = (struct GOBFileTableExtEntry*)
			MemFuncs.alloc(num_files * sizeof(struct GOBFileTableExtEntry));
		if (!FileTableExt) return GOBERR_NO_MEMORY;
	}
	else {
		FileTableExt = NULL;
	}

	// clear the tables
	for (num = 0; num < num_files; ++num) {
		FileTableBasic[num].block = GOB_INVALID_BLOCK;
		if (FileTableExt) FileTableExt[num].name[0] = 0;
	}

	for (num = 0; num < num_blocks; ++num) {
		BlockTable[num].next = GOB_INVALID_BLOCK;
		BlockTable[num].size = GOB_INVALID_SIZE;
	}

	return GOBERR_OK;
}


// GOBInit
// Public function.  Initialize the library.
GOBError GOBInit(struct GOBMemoryFuncSet* mem, 
	struct GOBFileSysFuncSet* file, 
	struct GOBCodecFuncSet* codec,
	struct GOBCacheFileFuncSet* cache)
{
	GOBInt32 i;
	GOBError err;
	
	if (LibraryInit) return GOBERR_ALREADY_INIT;
	
	// setup the callbacks
	MemFuncs = *mem;
	FSFuncs = *file;
	CodecFuncs = *codec;
	if (cache) {
		CacheFileFuncs = *cache;
		CacheFileActive = GOB_TRUE;
	} else {
		CacheFileActive = GOB_FALSE;
	}

	// allocate decompression buffer
	DecompBuffer = (GOBChar*)MemFuncs.alloc(GOB_BLOCK_SIZE + GOB_COMPRESS_OVERHEAD);
	if (!DecompBuffer) return GOBERR_NO_MEMORY;

	// clear open table
	for (i = 0; i < GOB_MAX_OPEN_FILES; ++i) {
		OpenFiles[i].valid = GOB_FALSE;
	}

	LibraryInit = GOB_TRUE;

	err = GOBSetCacheSize(1);
	if (err != GOBERR_OK) {
		LibraryInit = GOB_FALSE;
		return err;
	}

	ReadBuffer.data = NULL;
	err = GOBSetReadBufferSize(128*1024);
	if (err != GOBERR_OK) {
		LibraryInit = GOB_FALSE;
		return err;
	}

	return GOBERR_OK;
}

// GOBShutdown
// Public function.  Close the library.
GOBError GOBShutdown(GOBVoid)
{
	if (!LibraryInit) return GOBERR_NOT_INIT;
	
	// if we have an open archive, close it
	if (!InvalidHandle(ArchiveHandle)) GOBArchiveClose();
	
	FreeCache();

	// free read ahead buffer
	if (ReadBuffer.data) {
		MemFuncs.free(ReadBuffer.data);
		ReadBuffer.data = NULL;
	}

	// free decompression buffer
	MemFuncs.free(DecompBuffer);

	// free the file and block tables
	DeallocTables();

	LibraryInit = GOB_FALSE;
	return GOBERR_OK;
}


// GOBArchiveCreate
// Public function.  Create an empty GFC and GOB.
GOBError GOBArchiveCreate(const GOBChar* file)
{
	GOBChar fname[GOB_MAX_FILE_NAME_LEN];
	GOBFSHandle handle;
	GOBError error;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (!InvalidHandle(ArchiveHandle)) return GOBERR_ALREADY_OPEN;

	// Allocate the max space for tables
	error = AllocTables(GOB_MAX_BLOCKS, GOB_MAX_FILES, GOB_TRUE, GOB_TRUE);
	if (GOBERR_OK != error) {
		return error;
	}
	
	// create an empty GFC
	snprintf(ControlFileName, GOB_MAX_FILE_NAME_LEN, "%s.gfc", file);

	ArchiveSize = 0;
	ArchiveNumBlocks = 0;
	ArchiveNumFiles = 0;
	CacheFileActive = GOB_FALSE;
	
	CommitFileTable();

	// create an empty GOB
	snprintf(fname, GOB_MAX_FILE_NAME_LEN, "%s.gob", file);
	handle = FSFuncs.open(fname, GOBACCESS_WRITE);
	if (InvalidHandle(handle)) return GOBERR_CANNOT_CREATE;
	
	FSFuncs.close(&handle);

	return GOBERR_OK;	
}

// GOBArchiveOpen
// Public function.  Open a GOB file and cache file tables.
GOBError GOBArchiveOpen(const GOBChar* file, GOBAccessType atype, 
	GOBBool extended, GOBBool safe)
{
	GOBChar fname[GOB_MAX_FILE_NAME_LEN];
	GOBFSHandle handle;
	GOBUInt32 magic;
	GOBUInt32 i;
	GOBError error;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (!InvalidHandle(ArchiveHandle)) return GOBERR_ALREADY_OPEN;

	// open the GFC
	snprintf(ControlFileName, GOB_MAX_FILE_NAME_LEN, "%s.gfc", file);
	handle = FSFuncs.open(ControlFileName, atype);
	if (InvalidHandle(handle)) return GOBERR_FILE_NOT_FOUND;

	// read and check the magic
	if (!FSFuncs.read(handle, &magic, sizeof(magic))) return GOBERR_FILE_READ;
	if (SwapBytes(magic) != GOB_MAGIC_IDENTIFIER) return GOBERR_NOT_GOB_FILE;

	// read the GOB archive size
	if (!FSFuncs.read(handle, &ArchiveSize, sizeof(ArchiveSize))) return GOBERR_FILE_READ;
	ArchiveSize = SwapBytes(ArchiveSize);

	// read the number of blocks
	if (!FSFuncs.read(handle, &ArchiveNumBlocks, sizeof(ArchiveNumBlocks))) return GOBERR_FILE_READ;
	ArchiveNumBlocks = SwapBytes(ArchiveNumBlocks);

	// read the number of files
	if (!FSFuncs.read(handle, &ArchiveNumFiles, sizeof(ArchiveNumFiles))) return GOBERR_FILE_READ;
	ArchiveNumFiles = SwapBytes(ArchiveNumFiles);
	
	// Allocate the space for tables
	if (atype == GOBACCESS_READ) {
		error = AllocTables(ArchiveNumBlocks, ArchiveNumFiles, extended, safe);
	}
	else {
		error = AllocTables(GOB_MAX_BLOCKS, GOB_MAX_FILES, extended, safe);
	}
	if (GOBERR_OK != error) {
		return error;
	}

	// read the block table
	if (ArchiveNumBlocks && 
		!FSFuncs.read(handle, BlockTable, 
		sizeof(struct GOBBlockTableEntry) * ArchiveNumBlocks))
	{
		return GOBERR_FILE_READ;
	}

	if (BlockCRC) {
		// read the block CRCs
		if (ArchiveNumBlocks && 
			!FSFuncs.read(handle, BlockCRC, 
			sizeof(GOBUInt32) * ArchiveNumBlocks))
		{
			return GOBERR_FILE_READ;
		}
	}
	else {
		// skip block CRCs
		FSFuncs.seek(handle, sizeof(GOBUInt32) * ArchiveNumBlocks, 
			GOBSEEK_CURRENT);
	}

	if (CacheFileActive)
	{
		// clear the block cache table
		for (i = 0; i < ArchiveNumBlocks / 32; ++i) {
			CacheFileTable[i] = 0;
		}
	}

	// open the cache file
	if (CacheFileActive && !CacheFileFuncs.open(ArchiveSize)) {
		CacheFileActive = GOB_FALSE;
	}

	// endian convert the table
	for (i = 0; i < ArchiveNumBlocks; ++i) {
		BlockTable[i].next = SwapBytes(BlockTable[i].next);
		BlockTable[i].offset = SwapBytes(BlockTable[i].offset);
		BlockTable[i].size = SwapBytes(BlockTable[i].size);

		if (BlockCRC) {
			BlockCRC[i] = SwapBytes(BlockCRC[i]);
		}
	}

	// read the basic file table
	if (ArchiveNumFiles &&
		!FSFuncs.read(handle, FileTableBasic, 
		sizeof(struct GOBFileTableBasicEntry) * ArchiveNumFiles))
	{
		return GOBERR_FILE_READ;
	}

	// endian convert the table
	for (i = 0; i < ArchiveNumFiles; ++i) {
		FileTableBasic[i].hash = SwapBytes(FileTableBasic[i].hash);
		FileTableBasic[i].block = SwapBytes(FileTableBasic[i].block);
		FileTableBasic[i].size = SwapBytes(FileTableBasic[i].size);
	}

	// if we have memory for the extended file table
	if (FileTableExt) {
		// read the table
		if (ArchiveNumFiles &&
			!FSFuncs.read(handle, FileTableExt, 
			sizeof(struct GOBFileTableExtEntry) * ArchiveNumFiles))
		{
			return GOBERR_FILE_READ;
		}

		// endian convert the table
		for (i = 0; i < ArchiveNumFiles; ++i) {
			FileTableExt[i].crc = SwapBytes(FileTableExt[i].crc);
			FileTableExt[i].time = SwapBytes(FileTableExt[i].time);
		}
	}
	
	FSFuncs.close(&handle);

	// open the GOB
	snprintf(fname, GOB_MAX_FILE_NAME_LEN, "%s.gob", file);
	ArchiveHandle = FSFuncs.open(fname, atype);
	if (InvalidHandle(ArchiveHandle)) return GOBERR_FILE_NOT_FOUND;

	// initialize stats gathering
	CurrentArchivePos = 0;
	ReadStats.bufferUsed = 0;
	ReadStats.bytesRead = 0;
	ReadStats.cacheBytesRead = 0;
	ReadStats.cacheBytesWrite = 0;
	ReadStats.totalSeeks = 0;
	ReadStats.farSeeks = 0;
	ReadStats.filesOpened = 0;

	return GOBERR_OK;
}

// GOBArchiveClose
// Public function.  Close an open GOB archive.
GOBError GOBArchiveClose(GOBVoid)
{
	GOBInt32 i;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;

	// close any open files
	for (i = 0; i < GOB_MAX_OPEN_FILES; ++i) {
		GOBClose(i);
	}

	// close the GOB
	FSFuncs.close(&ArchiveHandle);
	ArchiveHandle = (GOBFSHandle*)0xFFFFFFFF;

	// commit the file table if we're updated it
	if (FileTableDirty) {
		CommitFileTable();
	}
	
	// close the cache file
	if (CacheFileActive) {
		CacheFileFuncs.close();
		CacheFileActive = GOB_FALSE;
	}

	return GOBERR_OK;
}

static int CDECL SortBlockDescsCallback(const void* elem1, const void* elem2)
{
	return (int)((struct GOBBlockTableEntry *)elem1)->offset - 
		(int)((struct GOBBlockTableEntry *)elem2)->offset;
}

// GOBArchiveCheckMarkers
// Public function.  Check start/end markers to check approximate validity of GOB file
GOBError GOBArchiveCheckMarkers(GOBVoid)
{
	GOBUInt32 i;
	GOBUInt32 valid_blocks;
	struct GOBBlockTableEntry *blocks;
	GOBUInt32 block;
	GOBUInt32 start_marker;
	GOBUInt32 end_marker;
	GOBBool ok;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;

	// count valid blocks
	valid_blocks = 0;
	for (i = 0; i < ArchiveNumBlocks; i++)
	{
		if (BlockTable[i].size != GOB_INVALID_SIZE && 
			BlockTable[i].next != GOB_INVALID_BLOCK)
		{
			valid_blocks++;
		}
	}

	// arcvive is empty
	if (valid_blocks == 0)
	{
		return GOBERR_OK;
	}

	// alloc mem for valid block list
	blocks = (GOBBlockTableEntry *) MemFuncs.alloc(sizeof(*blocks) * valid_blocks);
	if (blocks == NULL)
	{
		return GOBERR_NO_MEMORY;
	}

	// copy valid blocks descriptions
	block = 0;
	for (i = 0; i < ArchiveNumBlocks; ++i)
	{
		if (BlockTable[i].size != GOB_INVALID_SIZE && 
			BlockTable[i].next != GOB_INVALID_BLOCK)
		{
			blocks[block++] = BlockTable[i];
		}
	}
	assert(block == valid_blocks);

	// and sort 'em
	qsort(blocks, valid_blocks, sizeof(*blocks), SortBlockDescsCallback);

	// suppress some warnings
	start_marker = 0;
	end_marker = 0;

	// now scan entire archive for start-of-block and end-of-block markers
	for (i = 0; i < valid_blocks; i++)
	{
		ok = GOB_TRUE;
		ok = ok && !FSFuncs.seek(ArchiveHandle, blocks[i].offset, GOBSEEK_START);
		ok = ok && FSFuncs.read(ArchiveHandle, &start_marker, sizeof(GOBUInt32)) == sizeof(GOBUInt32);
		ok = ok && !FSFuncs.seek(ArchiveHandle, blocks[i].offset + blocks[i].size - sizeof(GOBUInt32), GOBSEEK_START);
		ok = ok && FSFuncs.read(ArchiveHandle, &end_marker, sizeof(GOBUInt32)) == sizeof(GOBUInt32);
		if (!ok || 
			SwapBytes(start_marker) != GOBMARKER_STARTBLOCK || 
			SwapBytes(end_marker) != GOBMARKER_ENDBLOCK)
		{
			MemFuncs.free(blocks);
			
			return GOBERR_NOT_GOB_FILE;
		}
	}
	
	MemFuncs.free(blocks);
	
	return GOBERR_OK;
}

// GOBArchiveCreate
// Public function.  Create an empty GFC and GOB.
GOBUInt32 GOBGetSlack(GOBUInt32 x)
{
	GOBUInt32 align = x % GOB_BLOCK_ALIGNMENT;
	if (align) return GOB_BLOCK_ALIGNMENT - align;
	return 0;
}

// GOBOpen
// Public function.  Open a file inside a GOB.
GOBError GOBOpen(GOBChar* file, GOBHandle* handle)
{
	GOBInt32 entry;
	GOBChar* lfile;
	
	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;

	// find a free handle
	for (*handle = 0; *handle < GOB_MAX_OPEN_FILES; (*handle) += 1) {
		if (!OpenFiles[*handle].valid) break;
	}

	if (*handle >= GOB_MAX_OPEN_FILES) return GOBERR_TOO_MANY_OPEN;

	// find the file in the table
	lfile = LowerCase(file);

	entry = GetFileTableEntry(lfile);

	if (entry == -1) return GOBERR_FILE_NOT_FOUND;

	// setup the open file
	OpenFiles[*handle].startBlock = OpenFiles[*handle].block = 
		FileTableBasic[entry].block;
	OpenFiles[*handle].size = FileTableBasic[entry].size;
	OpenFiles[*handle].offset = 0;
	OpenFiles[*handle].pos = 0;

	OpenFiles[*handle].valid = GOB_TRUE;

	++ReadStats.filesOpened;
	
	return GOBERR_OK;
}

// GOBOpenCode
// Public function.  Open file with a code inside a GOB.
GOBError GOBOpenCode(GOBInt32 code, GOBHandle* handle)
{	
	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;

	// find a free handle
	for (*handle = 0; *handle < GOB_MAX_OPEN_FILES; (*handle) += 1) {
		if (!OpenFiles[*handle].valid) break;
	}

	if (*handle >= GOB_MAX_OPEN_FILES) return GOBERR_TOO_MANY_OPEN;

	// setup the open file
	OpenFiles[*handle].startBlock = OpenFiles[*handle].block = 
		FileTableBasic[code].block;
	OpenFiles[*handle].size = FileTableBasic[code].size;
	OpenFiles[*handle].offset = 0;
	OpenFiles[*handle].pos = 0;

	OpenFiles[*handle].valid = GOB_TRUE;

	++ReadStats.filesOpened;
	
	return GOBERR_OK;
}

// GOBClose
// Public function.  Close a file.
GOBError GOBClose(GOBHandle handle)
{
	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	if (!OpenFiles[handle].valid) return GOBERR_NOT_OPEN;
	
	// close the file by simply invalidating the open
	// file table entry
	OpenFiles[handle].valid = GOB_FALSE;
	
	return GOBERR_OK;
}

static GOBUInt32 RawRead(GOBVoid* buffer, GOBUInt32 size, GOBUInt32 pos)
{
	GOBUInt32 bytes;

	// Reads _must_ be aligned otherwise things get very slow
	if (pos % GOB_BLOCK_ALIGNMENT) {
		return 0;
	}
	if ((GOBUInt32)buffer % GOB_MEM_ALIGNMENT) {
		return 0;
	}
	
	// seek
	if (FSFuncs.seek(ArchiveHandle, pos, GOBSEEK_START)) return 0;
	
	if (CurrentArchivePos != pos) ++ReadStats.totalSeeks;
	if (pos > CurrentArchivePos + GOB_BLOCK_ALIGNMENT ||
		CurrentArchivePos > pos + GOB_BLOCK_ALIGNMENT)
	{
		++ReadStats.farSeeks;
	}

	// read
	bytes = FSFuncs.read(ArchiveHandle, buffer, size);
	
	ReadStats.bytesRead += bytes;
	CurrentArchivePos = pos + bytes;
	
	return bytes;
}

static GOBUInt32 CacheRawRead(GOBVoid* buffer, GOBUInt32 size, GOBUInt32 pos)
{
	GOBUInt32 bytes;

	// Reads _must_ be aligned otherwise things get very slow
	if (pos % GOB_BLOCK_ALIGNMENT) {
		return 0;
	}
	if ((GOBUInt32)buffer % GOB_MEM_ALIGNMENT) {
		return 0;
	}
	
	// seek
	if (CacheFileFuncs.seek(pos)) return 0;
	
	// read
	bytes = CacheFileFuncs.read(buffer, size);
	ReadStats.cacheBytesRead += bytes;

	return bytes;
}

static GOBUInt32 CacheRawWrite(GOBVoid* buffer, GOBUInt32 size, GOBUInt32 pos)
{
	GOBUInt32 bytes;

	// Writes _must_ be aligned otherwise things get very slow
	if (pos % GOB_BLOCK_ALIGNMENT) {
		return 0;
	}
	if ((GOBUInt32)buffer % GOB_MEM_ALIGNMENT) {
		return 0;
	}
	
	// seek
	if (CacheFileFuncs.seek(pos)) return 0;
	
	// write
	bytes = CacheFileFuncs.write(buffer, size);	
	ReadStats.cacheBytesWrite += bytes;
	
	return bytes;
}

static GOBInt32 BlockReadLow(GOBUInt32 block)
{
	GOBUInt32 pos;
	GOBUInt32 bytes;
	GOBBool cache_read;
	GOBBool cache_write;
	GOBBool cache_fail;

	pos = 0;
	cache_read = GOB_FALSE;
	cache_write = GOB_FALSE;
	cache_fail = GOB_FALSE;
	
	for (;;) {	
		// is the block in the read ahead buffer?
		if (ReadBuffer.pos <= BlockTable[block].offset + pos &&
			ReadBuffer.pos + ReadBuffer.size > BlockTable[block].offset + pos)
		{
			GOBUInt32 buffer_offset;
			GOBUInt32 buffer_size;
			
			// use data in the read buffer
			buffer_offset = BlockTable[block].offset + pos - ReadBuffer.pos;
			buffer_size = ReadBuffer.size - buffer_offset;

			// clamp size
			if (buffer_size > BlockTable[block].size - pos) {
				buffer_size = BlockTable[block].size - pos;
			}
			
			memcpy(&DecompBuffer[pos], &ReadBuffer.dataStart[buffer_offset], buffer_size);

			pos += buffer_size;
		}

		// got enough data
		if (pos == BlockTable[block].size) break;

		// refill read buffer
		ReadBuffer.pos = BlockTable[block].offset + pos;
		ReadBuffer.pos -= ReadBuffer.pos % GOB_BLOCK_ALIGNMENT;

		// check if block is in the external cache system
		if (CacheFileActive &&
			CacheFileTable[block / 32] & (1 << (block % 32)))
		{
			if (CacheRawRead(ReadBuffer.dataStart, 
				ReadBuffer.size, ReadBuffer.pos))
			{
				cache_read = GOB_TRUE;
				continue;
			}
		}

		// read block from archive
		bytes = RawRead(ReadBuffer.dataStart, ReadBuffer.size, ReadBuffer.pos);
		if (bytes != ReadBuffer.size && 
			bytes != ArchiveSize - ReadBuffer.pos) 
		{
			return -1; // Main read fail error code
		}
		
		// write block to cache file
		if (CacheFileActive)
		{
			if (CacheRawWrite(ReadBuffer.dataStart, bytes, 
				ReadBuffer.pos) == bytes)
			{
				cache_write = GOB_TRUE;
			}
			else
			{
				cache_fail = GOB_TRUE;
			}
		}
	}

	if (cache_write) {
		if (!cache_fail) return 2;
		return 0;
	}

	if (cache_read) return 1;
	return 0;
}

static GOBBool BlockReadWithCache(GOBUInt32 block)
{
	GOBInt32 i;
	
	for (i = 0; i < GOB_READ_RETRYS; ++i) {
		GOBInt32 result;

		// read the data
		result = BlockReadLow(block);
		if (result >= 0)
		{
			if (BlockCRC) {
				// crc check
				GOBUInt32 crc;

				crc = adler32(0L, Z_NULL, 0);
				crc = adler32(crc, (const unsigned char*)DecompBuffer, 
					BlockTable[block].size);
				
				if (BlockCRC[block] != crc) {
					// crc mismatch, we must have got bad data --
					// try invalidating the cache and retrying...
					if (CacheFileActive) {
						CacheFileTable[block / 32] &= ~(1 << (block % 32));
					}
					ReadBuffer.pos = 0xFFFFFFFF;
					continue;
				}
			}

			// if cache write occurred -- mark block as cached
			if (result == 2) {
				CacheFileTable[block / 32] |= (1 << (block % 32));
			}
			
			// read success, crc success (or no check performed)
			return GOB_TRUE;
		}
	}

	// multiple read/crc failures
	return GOB_FALSE;
}

static GOBUInt32 BlockRead(GOBVoid* buffer, GOBUInt32 block)
{
	GOBUInt32 size;
	GOBInt32 codec_index;
	GOBChar *compressed_data;
	
	// read block from cache or archive
	if (!BlockReadWithCache(block))
	{
		return GOB_INVALID_SIZE;
	}
	
	// decompress
	codec_index = 0;
	size = 0; // Initialize to satisfy compiler
	compressed_data = DecompBuffer + sizeof(GOBUInt32); // skip start-of-block marker
	while (codec_index < CodecFuncs.codecs) {
		// Check if codec matches
		if (*compressed_data == CodecFuncs.codec[codec_index].tag) {
			size = GOB_BLOCK_SIZE;
			if (CodecFuncs.codec[codec_index].decompress(compressed_data + 1, 
				BlockTable[block].size - 1 - sizeof(GOBUInt32) * 2, buffer, &size)) {
				return GOB_INVALID_SIZE;
			}
			break;
		}
		codec_index++;
	}

	// If no suitable codecs were found, we're screwed
	if (codec_index == CodecFuncs.codecs) {
		return GOB_INVALID_SIZE;
	}

	if (ProfileReadCallback && ProfileEnabled) {
		// register current read command
		ProfileReadCallback(block);
	}

	return size;
}

static GOBVoid FillCacheBlock(GOBUInt32 block, GOBUInt32 index)
{
	CacheBlocks[index].time = CacheBlockCounter++;
	CacheBlocks[index].block = block;
	CacheBlocks[index].size = BlockRead(CacheBlocks[index].data, block);
}

static GOBInt32 FindBestCacheBlock(GOBUInt32 block)
{
	GOBInt32 i;
	GOBUInt32 oldest_time;
	GOBInt32 oldest_index;

	oldest_time = 0xFFFFFFFF;
	oldest_index = -1;

	for (i = 0; i < (signed)NumCacheBlocks; ++i) {
		if (CacheBlocks[i].block == block) {
			// if block is in this read buffer, use it
			return i;
		}
		
		// find the buffer that hasn't been accessed
		// for the longest time
		if (CacheBlocks[i].time < oldest_time) {
			oldest_time = CacheBlocks[i].time;
			oldest_index = i;
		}
	}

	// use the buffer that hasn't been accessed
	// in the longest time
	return oldest_index;
}

// GOBRead
// Public function.  Read from an open file using
// a funky read-ahead buffer system.
GOBUInt32 GOBRead(GOBVoid* buffer, GOBUInt32 size, GOBHandle handle)
{
	GOBUInt32 pos;
	GOBInt32 cache_id;
	
	if (!LibraryInit) return 0;
	if (InvalidHandle(ArchiveHandle)) return 0;
	if (!OpenFiles[handle].valid) return 0;

	// make sure we're reading within the file
	if (OpenFiles[handle].pos + size > OpenFiles[handle].size) {
		size = OpenFiles[handle].size - OpenFiles[handle].pos;
		if (!size) return 0;
	}
	
	cache_id = FindBestCacheBlock(OpenFiles[handle].block);
	if (cache_id < 0) return GOB_INVALID_SIZE;

	pos = OpenFiles[handle].pos;

	for (;;) {
		// are looking for data inside the read buffer?
		if (CacheBlocks[cache_id].block == OpenFiles[handle].block) {
			// move any relevant data from the read buffer to the target buffer
			GOBUInt32 buffer_size;
			
			// calc size of data we want from current buffer
			buffer_size = CacheBlocks[cache_id].size - OpenFiles[handle].offset;
			if (buffer_size > size) buffer_size = size;
			
			// move from read buffer into output buffer
			memcpy(&((char*)buffer)[OpenFiles[handle].pos - pos], 
				&CacheBlocks[cache_id].data[OpenFiles[handle].offset], 
				buffer_size);

			// update file position
			OpenFiles[handle].pos += buffer_size;
			OpenFiles[handle].offset += buffer_size;
			
			// if we've completed this block -- move to next
			if (OpenFiles[handle].offset == CacheBlocks[cache_id].size) {
				OpenFiles[handle].block = BlockTable[OpenFiles[handle].block].next;
				OpenFiles[handle].offset = 0;
			}
			
			CacheBlocks[cache_id].time = CacheBlockCounter++;
		
			ReadStats.bufferUsed += buffer_size;
			size -= buffer_size;
			if (size == 0) break;
		}

		// refill the buffer
		FillCacheBlock(OpenFiles[handle].block, cache_id);
		if (CacheBlocks[cache_id].size == GOB_INVALID_SIZE) {
			CacheBlocks[cache_id].block = GOB_INVALID_BLOCK;
			return GOB_INVALID_SIZE;
		}

		// reading off the end of the archive
		if (CacheBlocks[cache_id].block != OpenFiles[handle].block) break;
	}
	
	return OpenFiles[handle].pos - pos;
}

// GOBSeek
// Public function.  Seek to a position in an open file.
GOBError GOBSeek(GOBHandle handle, GOBUInt32 offset, GOBSeekType type, GOBUInt32* pos)
{
	GOBUInt32 blocks;
	
	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	if (!OpenFiles[handle].valid) return GOBERR_NOT_OPEN;
	
	// find a new position based on the seek type
	switch (type) {
	case GOBSEEK_START:
		*pos = offset;
		break;

	case GOBSEEK_CURRENT:
		*pos = OpenFiles[handle].pos + offset;
		break;

	case GOBSEEK_END:
		*pos = OpenFiles[handle].size + offset;
		break;

	default:
		return GOBERR_INVALID_SEEK;
	}

	// check to make sure we're still in the file
	if (*pos > OpenFiles[handle].size) {
		return GOBERR_INVALID_SEEK;
	}

	// update the file position
	OpenFiles[handle].pos = *pos;

	// update block
	blocks = *pos / GOB_BLOCK_SIZE;
	OpenFiles[handle].block = OpenFiles[handle].startBlock;
	while (blocks--) {
		OpenFiles[handle].block = BlockTable[OpenFiles[handle].block].next;
	}

	// update position inside block
	OpenFiles[handle].offset = *pos % GOB_BLOCK_SIZE;

	return GOBERR_OK;
}


static GOBUInt32 FindFreeBlock(GOBVoid)
{
	GOBInt32 i;
	for (i = 0; i < GOB_MAX_BLOCKS; ++i) {
		if (BlockTable[i].next == GOB_INVALID_BLOCK) return i;
	}
	return GOB_MAX_BLOCKS;
}

// GOBWrite
// Public function.  Write an entire file.  The file should not be open!
GOBError GOBWrite(GOBVoid* buffer, GOBUInt32 size, GOBUInt32 mtime, const GOBChar* file, GOBUInt32 codec_mask)
{
	GOBHandle handle;
	GOBInt32 slack;
	GOBChar* lfile;
	GOBUInt32 hash;
	GOBUInt32 crc;
	GOBInt32 i;
	GOBChar* out;
	GOBUInt32 pos;
	GOBUInt32 last_block;
	GOBInt32 codec_index;
	GOBInt32 compression_ratio;
	GOBChar* out_data;
	GOBUInt32 compressed_size;
	
	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	if (!FileTableExt) return GOBERR_NO_EXTENDED;
	if (!BlockCRC) return GOBERR_NO_EXTENDED;

	InvalidateCache();

	// delete the file if it exists
	GOBDelete(file);

	// find a free entry in the file table
	for (handle = 0; handle < GOB_MAX_FILES; ++handle) {
		if (FileTableBasic[handle].block == GOB_INVALID_BLOCK) break;
	}

	if (handle >= GOB_MAX_FILES) return GOBERR_TOO_MANY_FILES;
	if (handle >= (GOBInt32)ArchiveNumFiles) ArchiveNumFiles = handle + 1;

	// move to the end of the GOB
	if (FSFuncs.seek(ArchiveHandle, 0, GOBSEEK_END)) {
		return GOBERR_FILE_WRITE;
	}
	
	// alloc compression buffer
	out = (GOBChar*)MemFuncs.alloc(GOB_BLOCK_SIZE + GOB_COMPRESS_OVERHEAD);

	last_block = GOB_MAX_BLOCKS - 1;

	for (pos = 0; pos < size; pos += GOB_BLOCK_SIZE) {
		GOBUInt32 block;
		GOBUInt32 in_size;

		// get a free block
		block = FindFreeBlock();
		if (block >= GOB_MAX_BLOCKS) return GOBERR_TOO_MANY_BLOCKS;
		if (block >= ArchiveNumBlocks) ArchiveNumBlocks = block + 1;

		// if this is not the first block, mark next block for the last block
		// else assign the first block in file table
		if (pos != 0) BlockTable[last_block].next = block;
		else FileTableBasic[handle].block = block;
		
		// invalidate the next block
		BlockTable[block].next = GOB_MAX_BLOCKS;

		// compute the decompressed block size
		in_size = size - pos;
		if (in_size > GOB_BLOCK_SIZE) in_size = GOB_BLOCK_SIZE;

		// compress block
		
		for (
			codec_index = 0;
			codec_index < CodecFuncs.codecs;
			codec_index++)
		{
			if ( ! (GOB_CODEC_MASK(codec_index) & codec_mask) )
			{
				// skip if this codec is not listed as one of the allowed ones
				continue;
			}
			BlockTable[block].size = GOB_BLOCK_SIZE + GOB_COMPRESS_OVERHEAD;
			out_data = out;
			*(GOBUInt32*)out_data = SwapBytes(GOBMARKER_STARTBLOCK);
			out_data += sizeof(GOBUInt32);
			*out_data = CodecFuncs.codec[codec_index].tag;
			out_data++;
			if (CodecFuncs.codec[codec_index].compress(&((GOBChar*)buffer)[pos],
				in_size, out_data, &BlockTable[block].size)) 
			{
				return GOBERR_COMPRESS_FAIL;
			}
			out_data += BlockTable[block].size;
			*(GOBUInt32*)out_data = SwapBytes(GOBMARKER_ENDBLOCK);
			out_data += sizeof(GOBUInt32);

			// Adjust for the prefixed start-of-block marker and codec tag and trailing end-of-block marker
			compressed_size = BlockTable[block].size;
			BlockTable[block].size += 1 + sizeof(GOBUInt32) * 2;

			// Check compression result
			compression_ratio = compressed_size * 100 / in_size;
			if (compression_ratio <= CodecFuncs.codec[codec_index].max_ratio)
			{
				// Compressed result is under par.  Let's go with it
				break;
			}

			// Otherwise, try the next compressor
		}

		// If no suitable codecs were found, take our ball and go home
		if (codec_index == CodecFuncs.codecs) return GOBERR_NO_SUITABLE_CODEC;

		// compute and store the CRC
		BlockCRC[block] = adler32(0L, Z_NULL, 0);
		BlockCRC[block] = adler32(BlockCRC[block], (const unsigned char*)out, 
					BlockTable[block].size);

		// write block
		if (FSFuncs.write(ArchiveHandle, out, BlockTable[block].size) != 
			(signed)BlockTable[block].size) 
		{
			return GOBERR_FILE_WRITE;
		}

		// compute the slack (to keep alignment)
		slack = GOBGetSlack(BlockTable[block].size);
	
		// write the slack space
		memset(out, 0, slack);
		if (FSFuncs.write(ArchiveHandle, out, slack) != slack) {
			return GOBERR_FILE_WRITE;
		}
	
		BlockTable[block].offset = ArchiveSize;
		ArchiveSize += BlockTable[block].size + slack;

		last_block = block;
	}

	MemFuncs.free(out);

	lfile = LowerCase(file);

	// calculate file name hash
	hash = crc32(0L, Z_NULL, 0);
	hash = crc32(hash, (const unsigned char*)lfile, strlen(lfile));
	
	// make sure hash is unique
	for (i = 0; i < GOB_MAX_FILES; ++i) {
		if (i != handle &&
			FileTableBasic[i].block != GOB_INVALID_BLOCK && 
			FileTableBasic[i].hash == hash) 
		{
			return GOBERR_DUP_HASH;
		}
	}

	// update the file tables
	FileTableBasic[handle].hash = hash;
	FileTableBasic[handle].size = size;

	strcpy(FileTableExt[handle].name, lfile);
	
	crc = crc32(0L, Z_NULL, 0);
	crc = crc32(crc, (const unsigned char*)buffer, size);
	FileTableExt[handle].crc = crc;
	
	FileTableExt[handle].time = mtime;

	FileTableDirty = GOB_TRUE;
	return GOBERR_OK;
}

// GOBDelete
// Public function.  Delete a file from a GOB.  The file should not be open!
GOBError GOBDelete(const GOBChar* file)
{
	GOBInt32 entry;
	GOBChar* lfile;
	GOBUInt32 block;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	if (!FileTableExt) return GOBERR_NO_EXTENDED;
	if (!BlockCRC) return GOBERR_NO_EXTENDED;

	// find the file in the table
	lfile = LowerCase(file);
	
	entry = GetFileTableEntry(lfile);
	if (entry == -1) return GOBERR_FILE_NOT_FOUND;

	// invalidate blocks
	block = FileTableBasic[entry].block;
	do {
		GOBUInt32 next;
		next = BlockTable[block].next;
		BlockTable[block].next = GOB_INVALID_BLOCK;
		block = next;
	 } while(block != GOB_MAX_BLOCKS);

	// invalidate the file
	FileTableBasic[entry].block = GOB_INVALID_BLOCK;

	FileTableDirty = GOB_TRUE;

	return GOBERR_OK;
}

// GOBRearrange
// Public function.  Sorts the blocks in an archive.
GOBError GOBRearrange(const GOBChar* file, const GOBUInt32* xlat, GOBFileSysRenameFunc _rename)
{
	GOBError err;
	GOBVoid* buffer;
	GOBInt32 slack;
	GOBVoid* slack_buf;
	GOBUInt32 i;
	GOBUInt32 size;
	GOBFSHandle temp_handle;
	GOBChar full_name[GOB_MAX_FILE_NAME_LEN];
	
	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (!InvalidHandle(ArchiveHandle)) return GOBERR_ALREADY_OPEN;
	if (!FileTableExt) return GOBERR_NO_EXTENDED;
	if (!BlockCRC) return GOBERR_NO_EXTENDED;

	// start things up
	err = GOBArchiveOpen(file, GOBACCESS_READ, GOB_TRUE, GOB_TRUE);
	if (err != GOBERR_OK) return err;

	// create temporary file
	temp_handle = FSFuncs.open("~temp.tmp", GOBACCESS_WRITE);
	if (InvalidHandle(temp_handle)) return GOBERR_FILE_WRITE;

	size = 0;

	// create an empty buffer for slack
	slack_buf = MemFuncs.alloc(GOB_BLOCK_ALIGNMENT);
	if (!slack_buf) return GOBERR_NO_MEMORY;
	memset(slack_buf, 0, GOB_BLOCK_ALIGNMENT);

	// get memory for block
	buffer = MemFuncs.alloc(GOB_BLOCK_SIZE + GOB_COMPRESS_OVERHEAD);
	if (!buffer) return GOBERR_NO_MEMORY;

	// copy files in new order to end of archive
	for (i = 0; i < ArchiveNumBlocks; ++i) {
		if (BlockTable[xlat[i]].next != GOB_INVALID_BLOCK) {
			// seek to the block
			if (FSFuncs.seek(ArchiveHandle, 
				BlockTable[xlat[i]].offset, GOBSEEK_START)) 
			{
				return GOBERR_FILE_READ;
			}
			
			// read the block
			if (FSFuncs.read(ArchiveHandle, buffer, BlockTable[xlat[i]].size) !=
				(signed)BlockTable[xlat[i]].size) 
			{
				return GOBERR_FILE_READ;
			}

			// write block
			if (FSFuncs.write(temp_handle, buffer, BlockTable[xlat[i]].size) !=
				(signed)BlockTable[xlat[i]].size) 
			{
				return GOBERR_FILE_WRITE;
			}
			
			// write the slack
			slack = GOBGetSlack(BlockTable[xlat[i]].size);
			if (FSFuncs.write(temp_handle, slack_buf, slack) != slack) {
				return GOBERR_FILE_WRITE;
			}
			
			// update block pos
			BlockTable[xlat[i]].offset = size;
			size += BlockTable[xlat[i]].size + slack;
		}
	}

	MemFuncs.free(buffer);
	MemFuncs.free(slack_buf);
	
	// close the archive
	err = GOBArchiveClose();
	if (err != GOBERR_OK) return err;

	// close temp file
	FSFuncs.close(&temp_handle);

	// overrwrite archive with temp file
	snprintf(full_name, GOB_MAX_FILE_NAME_LEN, "%s.gob", file);
	if (_rename("~temp.tmp", full_name)) return GOBERR_FILE_RENAME;

	ArchiveSize = size;

	CommitFileTable();
			
	return GOBERR_OK;
}


// GOBVerify
// Public function.  Verifies the integrity of a file.
GOBError GOBVerify(const GOBChar* file, GOBBool* status)
{
	GOBHandle handle;
	GOBError err;
	GOBVoid* buffer;
	GOBUInt32 size, junk;
	GOBUInt32 crc;
	GOBInt32 entry;
	GOBChar* lfile;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	if (!FileTableExt) return GOBERR_NO_EXTENDED;
	if (!BlockCRC) return GOBERR_NO_EXTENDED;

	// get the file size
	size = 0; // assign to avoid compiler warning
	err = GOBGetSize(file, &size, &junk, &junk);
	if (err != GOBERR_OK) return err;
	
	// open the file
	err = GOBOpen((GOBChar*)file, &handle);
	if (err != GOBERR_OK) return err;

	lfile = LowerCase(file);
	entry = GetFileTableEntry(lfile);

	// alloc space for the file 
	buffer = MemFuncs.alloc(size);
	if (!buffer) return GOBERR_NO_MEMORY;

	// read it into the buffer
	crc = GOBRead(buffer, size, handle);
	if (crc != size) return GOBERR_FILE_READ;
	
	// calc the crc
	crc = crc32(0L, Z_NULL, 0);
	crc = crc32(crc, (const unsigned char*)buffer, size);
	
	MemFuncs.free(buffer);

	// verify the crc matches
	if (crc != FileTableExt[entry].crc) *status = GOB_FALSE;
	else *status = GOB_TRUE;
	
	err = GOBClose(handle);
	if (err != GOBERR_OK) return err;
	
	return GOBERR_OK;
}

// GOBGetSize
// Public function.  Get a file compressed, decompressed, slack sizes.
GOBError GOBGetSize(const GOBChar* file, 
	GOBUInt32* decomp, GOBUInt32* comp, GOBUInt32* slack)
{
	GOBInt32 entry;
	GOBChar* lfile;
	GOBUInt32 block;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;

	// get file table entry
	lfile = LowerCase(file);
	entry = GetFileTableEntry(lfile);
	if (entry == -1) return GOBERR_FILE_NOT_FOUND;

	// decompressed size from file table
	*decomp = FileTableBasic[entry].size;
	
	// compressed size is sum of block sizes
	*comp = 0;
	*slack = 0;
	block = FileTableBasic[entry].block;
	while (block != GOB_MAX_BLOCKS) {
		*comp += BlockTable[block].size;
		*slack += GOBGetSlack(BlockTable[block].size);
		block = BlockTable[block].next;
	}

	return GOBERR_OK;
}

// GOBGetTime
// Public function.  Get a file modification time.
GOBError GOBGetTime(const GOBChar* file, GOBUInt32* time)
{
	GOBInt32 entry;
	GOBChar* lfile;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	if (!FileTableExt) return GOBERR_NO_EXTENDED;
	if (!BlockCRC) return GOBERR_NO_EXTENDED;

	lfile = LowerCase(file);
	entry = GetFileTableEntry(lfile);
	if (entry == -1) return GOBERR_FILE_NOT_FOUND;

	*time = FileTableExt[entry].time;
	return GOBERR_OK;
}

// GOBGetCRC
// Public function.  Get a file CRC.
GOBError GOBGetCRC(const GOBChar* file, GOBUInt32* crc)
{
	GOBInt32 entry;
	GOBChar* lfile;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	if (!FileTableExt) return GOBERR_NO_EXTENDED;
	if (!BlockCRC) return GOBERR_NO_EXTENDED;

	lfile = LowerCase(file);
	entry = GetFileTableEntry(lfile);
	if (entry == -1) return GOBERR_FILE_NOT_FOUND;

	*crc = FileTableExt[entry].crc;
	return GOBERR_OK;
}

// GOBAccess
// Public function.  Determine if a file exists in the archive.
GOBError GOBAccess(const GOBChar* file, GOBBool* status)
{
	GOBInt32 entry;
	GOBChar* lfile;

	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;

	lfile = LowerCase(file);
	entry = GetFileTableEntry(lfile);
	if (entry == -1) *status = GOB_FALSE;
	else *status = GOB_TRUE;

	return GOBERR_OK;
}

// GOBGetFileCode
// Public function.  Find the index into the file table of a file.
GOBInt32 GOBGetFileCode(const GOBChar* file)
{
	GOBInt32 entry;
	GOBChar* lfile;

	if (!LibraryInit) return -1;
	if (InvalidHandle(ArchiveHandle)) return -1;

	lfile = LowerCase(file);
	entry = GetFileTableEntry(lfile);

	return entry;
}

// GOBGetFileTables
// Public function.  Return the active file tables.
GOBError GOBGetFileTables(struct GOBFileTableBasicEntry** basic, 
	struct GOBFileTableExtEntry** ext)
{
	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	*basic = FileTableBasic;
	*ext = FileTableExt;
	return GOBERR_OK;
}

// GOBGetBlockTable
// Public function.  Return the active block table.
GOBError GOBGetBlockTable(struct GOBBlockTableEntry** table, GOBUInt32* num)
{
	if (!LibraryInit) return GOBERR_NOT_INIT;
	if (InvalidHandle(ArchiveHandle)) return GOBERR_NOT_OPEN;
	*table = BlockTable;
	*num = ArchiveNumBlocks;
	return GOBERR_OK;
}

// GOBSetCacheSize
// Public function.  Allocates buffers to cache blocks.
GOBError GOBSetCacheSize(GOBUInt32 num)
{
	GOBUInt32 i;

	if (!LibraryInit) return GOBERR_NOT_INIT;

	// only continue if we actually need to resize
	if (num == NumCacheBlocks) return GOBERR_OK;

	// free old cache buffers
	FreeCache();

	NumCacheBlocks = 0;
	
	CacheBlocks = (struct GOBBlockCache*)MemFuncs.alloc(
			sizeof(struct GOBBlockCache) * num);
	if (!CacheBlocks) return GOBERR_NO_MEMORY;

	// allocate cache blocks and initialize
	for (i = 0; i < num; ++i) {
		CacheBlocks[i].data = (GOBChar*)MemFuncs.alloc(GOB_BLOCK_SIZE);
		if (!CacheBlocks[i].data) return GOBERR_NO_MEMORY;
		
		CacheBlocks[i].size = 0;
		CacheBlocks[i].time = 0;
		CacheBlocks[i].block = 0xFFFFFFFF;
		
		++NumCacheBlocks;
	}
	
	return GOBERR_OK;
}

// GOBSetReadBufferSize
// Public function.  Allocate a read ahead buffer.
GOBError GOBSetReadBufferSize(GOBUInt32 size)
{
	if (!LibraryInit) return GOBERR_NOT_INIT;

	// only continue if we actually need to resize
	if (size == ReadBuffer.size) return GOBERR_OK;

	// remove old buffer
	if (ReadBuffer.data) MemFuncs.free(ReadBuffer.data);

	// allocate new buffer
	ReadBuffer.data = (GOBChar*)MemFuncs.alloc(size + GOB_MEM_ALIGNMENT);
	if (!ReadBuffer.data) return GOB_INVALID_SIZE;
	
	// set aligned pointer
	ReadBuffer.dataStart = 
		&ReadBuffer.data[GOB_MEM_ALIGNMENT - 
		((GOBUInt32)(ReadBuffer.data) % GOB_MEM_ALIGNMENT)];

	ReadBuffer.pos = 0xFFFFFFFF;
	ReadBuffer.size = size;
	
	return GOBERR_OK;
}

// GOBGetReadStats
// Public function.  Get file read statistics (seeks, sizes).
struct GOBReadStats GOBGetReadStats(GOBVoid)
{
	return ReadStats;
}


GOBVoid GOBSetProfileFuncs(struct GOBProfileFuncSet* fset)
{
	ProfileReadCallback = fset->read;
}

GOBError GOBStartProfile(GOBVoid)
{
	if (ProfileEnabled) return GOBERR_PROFILE_ON;
	ProfileEnabled = GOB_TRUE;
	return GOBERR_OK;
}

GOBError GOBStopProfile(GOBVoid)
{
	if (!ProfileEnabled) return GOBERR_PROFILE_OFF;
	ProfileEnabled = GOB_FALSE;
	return GOBERR_OK;
}
