/*
   Copyright (C) 1997-2001 Id Software, Inc.

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

   See the GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 */

#include "qcommon.h"

#include "sys_fs.h"

#include "zlib.h"
#include "md5.h"

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

   QUAKE FILESYSTEM

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

#define FS_ZIP_BUFSIZE		    0x00001000

#define FS_ZIP_BUFREADCOMMENT	    0x00000400
#define FS_ZIP_SIZELOCALHEADER	    0x0000001e
#define FS_ZIP_SIZECENTRALDIRITEM   0x0000002e

#define FS_ZIP_LOCALHEADERMAGIC	    0x04034b50
#define FS_ZIP_CENTRALHEADERMAGIC   0x02014b50
#define FS_ZIP_ENDHEADERMAGIC	    0x06054b50

static const char *forbidden_gamedirs[] = {
	"docs",
	"libs",
	"browser",
	APPLICATION ".app",
	NULL
};

typedef struct
{
	unsigned char readBuffer[FS_ZIP_BUFSIZE]; // internal buffer for compressed data
	z_stream zstream;                       // zLib stream structure for inflate
	unsigned compressedSize;
	unsigned restReadCompressed;            // number of bytes to be decompressed
} zipEntry_t;

#define FS_PACKFILE_DEFLATED	    1
#define FS_PACKFILE_COHERENT	    2

typedef struct packfile_s
{
	char *name;
	unsigned flags;
	unsigned compressedSize;    // compressed size
	unsigned uncompressedSize;  // uncompressed size
	unsigned offset;            // relative offset of local header
	struct packfile_s *hashNext;
} packfile_t;

//
// in memory
//
typedef struct
{
	char *filename;     // full path
	unsigned checksum;
	qboolean pure;
	void *sysHandle;
	int numFiles;
	int hashSize;
	packfile_t *files;
	char *fileNames;
	packfile_t **filesHash;
} pack_t;

typedef struct filehandle_s
{
	FILE *fstream;
	unsigned offset;
	unsigned uncompressedSize;      // uncompressed size
	unsigned restReadUncompressed;  // number of bytes to be obtained after decompession
	zipEntry_t *zipEntry;
	struct filehandle_s *prev, *next;
} filehandle_t;

typedef struct searchpath_s
{
	char *path;                     // set on both, packs and directories, won't include the pack name, just path
	pack_t *pack;
	struct	    searchpath_s *next;
} searchpath_t;

typedef struct
{
	char *name;
	searchpath_t *searchPath;
} searchfile_t;

static searchfile_t *fs_searchfiles;
static int fs_numsearchfiles;
static int fs_cursearchfiles;

static cvar_t *fs_basepath;
static cvar_t *fs_cdpath;
static cvar_t *fs_usehomedir;
static cvar_t *fs_basegame;
static cvar_t *fs_game;

// these are used in couple of functions to temporary store a full path to filenames
// so that it doesn't need to be constantly reallocated
static char *tempname = NULL;
static size_t tempname_size = 0;

static searchpath_t *fs_basepaths = NULL;       // directories without gamedirs
static searchpath_t *fs_searchpaths = NULL;     // game search directories, plus paks
static searchpath_t *fs_base_searchpaths;       // same as above, but without extra gamedirs

static mempool_t *fs_mempool;

#define FS_Malloc( size ) Mem_Alloc( fs_mempool, size )
#define FS_Realloc( data, size ) Mem_Realloc( data, size )
#define FS_Free( data ) Mem_Free( data )

#define FS_MAX_BLOCK_SIZE   0x10000
#define FS_MAX_HASH_SIZE    1024
#define FS_MAX_HANDLES	    1024

static filehandle_t fs_filehandles[FS_MAX_HANDLES];
static filehandle_t fs_filehandles_headnode, *fs_free_filehandles;

// we mostly read from one file at a time, so keep a global copy of one
// zipEntry to save on malloc calls and linking in FS_FOpenFile
static zipEntry_t fs_globalZipEntry;
static filehandle_t *fs_globalZipEntryUser;

static qboolean	fs_initialized = qfalse;

/*

   All of Quake's data access is through a hierchal file system, but the contents of the file system
   can be transparently merged from several sources.

 */

static inline unsigned int LittleLongRaw( const qbyte *raw )
{
	return ( raw[3] << 24 ) | ( raw[2] << 16 ) | ( raw[1] << 8 ) | raw[0];
}

static inline unsigned short LittleShortRaw( const qbyte *raw )
{
	return ( raw[1] << 8 ) | raw[0];
}

/*
   ===========
   FS_PackHashKey
   ===========
 */
static unsigned FS_PackHashKey( const char *str, int hashSize )
{
	int c;
	unsigned hashval = 0;

	while( ( c = *str++ ) != 0 )
	{
		if( c == '\\' )
			c = '/';
		hashval = hashval*37 + tolower( c );
	}
	return hashval & ( hashSize - 1 );
}

/*
   =================
   FS_PK3CheckFileCoherency

   Read the local header of the current zipfile
   Check the coherency of the local header and info in the end of central directory about this file
   =================
 */
static unsigned FS_PK3CheckFileCoherency( FILE *f, packfile_t *file )
{
	unsigned flags;
	unsigned char localHeader[31], compressed;

	if( fseek( f, file->offset, SEEK_SET ) != 0 )
		return 0;
	if( fread( localHeader, 1, sizeof( localHeader ), f ) != sizeof( localHeader ) )
		return 0;

	// check the magic
	if( LittleLongRaw( &localHeader[0] ) != FS_ZIP_LOCALHEADERMAGIC )
		return 0;
	compressed = LittleShortRaw( &localHeader[8] );
	if( ( compressed == Z_DEFLATED ) && !( file->flags & FS_PACKFILE_DEFLATED ) )
		return 0;
	else if( !compressed && ( file->flags & FS_PACKFILE_DEFLATED ) )
		return 0;

	flags = LittleShortRaw( &localHeader[6] ) & 8;
	if( ( LittleLongRaw( &localHeader[18] ) != file->compressedSize ) && !flags )
		return 0;
	if( ( LittleLongRaw( &localHeader[22] ) != file->uncompressedSize ) && !flags )
		return 0;

	return FS_ZIP_SIZELOCALHEADER + LittleShortRaw( &localHeader[26] ) + ( unsigned )LittleShortRaw( &localHeader[28] );
}

static int FS_SortStrings( const char **first, const char **second )
{
	return Q_stricmp( *first, *second );
}

/*
   ================
   FS_CopyString
   ================
 */
static char *FS_CopyString( const char *in )
{
	int size;
	char *out;

	size = sizeof( char ) * ( strlen( in ) + 1 );
	out = FS_Malloc( size );
	Q_strncpyz( out, in, size );

	return out;
}

/*
   ================
   FS_CheckTempnameSize
   ================
 */
static inline void FS_CheckTempnameSize( size_t size )
{
	//static int calls = 0;
	//static int allocs = 0;

	//calls++;
	if( tempname_size < size )
	{
		if( tempname )
			FS_Free( tempname );
		tempname = FS_Malloc( size );
		tempname_size = size;
		//allocs++;
	}
	//Com_Printf( "calls: %i, allocs: %i\n", calls, allocs );
}

/*
   ================
   FS_ListFiles
   ================
 */
static char **FS_ListFiles( char *findname, int *numfiles, unsigned musthave, unsigned canthave )
{
	const char *s;
	int nfiles = 0;
	static char **list = NULL;

	s = Sys_FS_FindFirst( findname, musthave, canthave );
	while( s )
	{
		if( COM_ValidateFilename( s ) )
			nfiles++;
		s = Sys_FS_FindNext( musthave, canthave );
	}
	Sys_FS_FindClose();

	if( !nfiles )
		return NULL;

	nfiles++; // add space for a guard
	*numfiles = nfiles;
	list = Mem_ZoneMalloc( sizeof( char * ) * nfiles );

	s = Sys_FS_FindFirst( findname, musthave, canthave );
	nfiles = 0;
	while( s )
	{
		if( !COM_ValidateFilename( s ) )
			continue;

		list[nfiles] = ZoneCopyString( s );

#ifdef _WIN32
		Q_strlwr( list[nfiles] );
#endif
		nfiles++;
		s = Sys_FS_FindNext( musthave, canthave );
	}
	Sys_FS_FindClose();

	return list;
}

/*
   ===========
   FS_SearchPathForFile

   Gives the searchpath element where this file exists, or NULL if it doesn't
   ===========
 */
static searchpath_t *FS_SearchPathForFile( const char *filename )
{
	searchpath_t *search;
	unsigned hashKey;
	qboolean purepass;
	FILE *f;

	if( !COM_ValidateRelativeFilename( filename ) )
		return NULL;

	// search through the path, one element at a time
	search = fs_searchpaths;
	purepass = qtrue;
	while( search )
	{
		if( search->pack && search->pack->pure == purepass )
		{                                                   // is the element a pak file?
			pack_t *pak = search->pack;
			packfile_t *pakFile;

			hashKey = FS_PackHashKey( filename, pak->hashSize );

			// look through all the pak file elements
			for( pakFile = pak->filesHash[hashKey]; pakFile; pakFile = pakFile->hashNext )
			{
				if( !Q_stricmp( filename, pakFile->name ) )
					return search; // found it!
			}
		}
		else if( !purepass )
		{
			FS_CheckTempnameSize( sizeof( char ) * ( strlen( search->path ) + 1 + strlen( filename ) + 1 ) );
			Q_snprintfz( tempname, tempname_size, "%s/%s", search->path, filename );

			f = fopen( tempname, "rb" );
			if( f )
			{
				fclose( f );
				return search;
			}
		}
		if( !search->next && purepass )
		{
			search = fs_searchpaths;
			purepass = qfalse;
		}
		else
		{
			search = search->next;
		}
	}

	return NULL;
}

/*
   ===========
   FS_SearchPathForBaseFile

   Gives the searchpath element where this file exists, or NULL if it doesn't
   ===========
 */
static searchpath_t *FS_SearchPathForBaseFile( const char *filename )
{
	FILE *f;
	searchpath_t *search;

	if( !COM_ValidateRelativeFilename( filename ) )
		return NULL;

	// search through the path, one element at a time
	search = fs_basepaths;
	while( search )
	{
		assert( !search->pack );

		FS_CheckTempnameSize( sizeof( char ) * ( strlen( search->path ) + 1 + strlen( filename ) + 1 ) );
		Q_snprintfz( tempname, tempname_size, "%s/%s", search->path, filename );

		f = fopen( tempname, "rb" );
		if( f )
		{
			fclose( f );
			return search;
		}

		search = search->next;
	}

	return NULL;
}

/*
   ===========
   FS_PakNameForFile
   ===========
 */
const char *FS_PakNameForFile( const char *filename )
{
	const char *p;
	searchpath_t *search = FS_SearchPathForFile( filename );

	if( !search || !search->pack )
		return NULL;

	// only give the basename part
	p = search->pack->filename + strlen( search->pack->filename ) - 1;
	while( p != search->pack->filename && *p != '/' )
		p--;
	if( p != search->pack->filename )
	{
		p--;
		while( p != search->pack->filename && *p != '/' )
			p--;
		if( p != search->pack->filename )
			p++;
	}

	return p;
}

/*
   ===========
   Cmd_PakFile_f
   ===========
 */
static void Cmd_PakFile_f( void )
{
	const char *s = FS_PakNameForFile( Cmd_Argv( 1 ) );

	if( !s )
		Com_Printf( "Pakfile: File is not loaded from pak file.\n" );
	else
		Com_Printf( "Pakfile: %s\n", s );
}

/*
   ===========
   FS_GetExplicitPurePakList
   ===========
 */
int FS_GetExplicitPurePakList( char ***paknames )
{
	searchpath_t *search;
	char *p;
	int numpaks, i;
	const char *pure_suffix = "pure.pk3";
	const size_t pure_suffix_len = strlen( pure_suffix );

	// count them
	numpaks = 0;
	for( search = fs_searchpaths; search; search = search->next )
	{
		if( !search->pack )
			continue;
		if( strlen( search->pack->filename ) <= pure_suffix_len )
			continue;

		if( !Q_stricmp( search->pack->filename + strlen( search->pack->filename ) - pure_suffix_len, pure_suffix ) )
			numpaks++;
	}

	if( !numpaks )
		return 0;

	*paknames = Mem_ZoneMalloc( sizeof( char * ) * numpaks );

	i = 0;
	for( search = fs_searchpaths; search; search = search->next )
	{
		if( !search->pack )
			continue;
		if( strlen( search->pack->filename ) <= pure_suffix_len )
			continue;

		if( !Q_stricmp( search->pack->filename + strlen( search->pack->filename ) - pure_suffix_len, pure_suffix ) )
		{
			assert( i < numpaks );

			// only give the basename part
			p = search->pack->filename + strlen( search->pack->filename ) - 1;
			while( p != search->pack->filename && *p != '/' )
				p--;
			if( p != search->pack->filename )
			{
				p--;
				while( p != search->pack->filename && *p != '/' )
					p--;
				if( p != search->pack->filename )
					p++;
			}

			( *paknames )[i] = ZoneCopyString( p );
			i++;
		}
	}
	assert( i == numpaks );

	return numpaks;
}

/*
   ===========
   FS_OpenFileHandle
   ===========
 */
static int FS_OpenFileHandle( void )
{
	filehandle_t *fh;

	if( !fs_free_filehandles )
		Sys_Error( "FS_OpenFileHandle: no free file handles" );

	fh = fs_free_filehandles;
	fs_free_filehandles = fh->next;

	// put the handle at the start of the list
	fh->prev = &fs_filehandles_headnode;
	fh->next = fs_filehandles_headnode.next;
	fh->next->prev = fh;
	fh->prev->next = fh;

	return ( fh - fs_filehandles ) + 1;
}

/*
   ===========
   FS_FileHandleForNum
   ===========
 */
static filehandle_t *FS_FileHandleForNum( int file )
{
	if( file < 1 || file > FS_MAX_HANDLES )
		Sys_Error( "FS_FileHandleForNum: bad handle" );
	if( !fs_filehandles[--file].fstream )
		Sys_Error( "FS_FileHandleForNum: bad handle" );
	return &fs_filehandles[file];
}

/*
   ==============
   FS_CloseFileHandle
   ==============
 */
static void FS_CloseFileHandle( filehandle_t *fh )
{
	// remove from linked open list
	fh->prev->next = fh->next;
	fh->next->prev = fh->prev;

	// insert into linked free list
	fh->next = fs_free_filehandles;
	fs_free_filehandles = fh;
}

/*
   ==============
   FS_FirstExtension
   Searches the paths for file matching with one of the extensions
   If found returns the extension otherwise NULL
   extensions parameter is string with extensions separated by spaces
   The caller must Mem_Free the returned string
   ==============
 */
const char *FS_FirstExtension( const char *filename, const char *extensions[], int num_extensions )
{
	char **filenames;           // slots for testable filenames
	size_t filename_size;       // size of one slot
	int i;
	size_t max_extension_length;
	searchpath_t *search;
	qboolean purepass;
	unsigned hashKey;
	FILE *f;

	assert( filename && extensions );

	if( !num_extensions )
		return NULL;

#ifndef NDEBUG
	for( i = 0; i < num_extensions; i++ )
		assert( extensions[i] && extensions[i][0] );
#endif

	if( !COM_ValidateRelativeFilename( filename ) )
		return NULL;

	max_extension_length = 0;
	for( i = 0; i < num_extensions; i++ )
	{
		if( strlen( extensions[i] ) > max_extension_length )
			max_extension_length = strlen( extensions[i] );
	}

	// set the filenames to be tested
	filenames = Mem_TempMalloc( sizeof( char * ) * num_extensions );
	filename_size = sizeof( char ) * ( strlen( filename ) + max_extension_length + 1 );
	for( i = 0; i < num_extensions; i++ )
	{
		if( i )
			filenames[i] = ( char * )( ( qbyte * )filenames[0] + filename_size * i );
		else
			filenames[i] = Mem_TempMalloc( filename_size * num_extensions );
		Q_strncpyz( filenames[i], filename, filename_size );
		COM_ReplaceExtension( filenames[i], extensions[i], filename_size );
	}

	// search through the path, one element at a time
	search = fs_searchpaths;
	purepass = qtrue;
	while( search )
	{
		if( search->pack && search->pack->pure == purepass ) // is the element a pak file?
		{
			pack_t *pak = search->pack;
			packfile_t *pakFile;

			for( i = 0; i < num_extensions; i++ )
			{
				hashKey = FS_PackHashKey( filenames[i], pak->hashSize );

				// look through all the pak file elements
				for( pakFile = pak->filesHash[hashKey]; pakFile; pakFile = pakFile->hashNext )
				{
					if( !Q_stricmp( filenames[i], pakFile->name ) )
					{
						Mem_TempFree( filenames[0] );
						Mem_TempFree( filenames );
						return extensions[i];
					}
				}
			}
		}
		else if( !purepass )
		{
			for( i = 0; i < num_extensions; i++ )
			{
				FS_CheckTempnameSize( sizeof( char ) * ( strlen( search->path ) + 1 + strlen( filenames[i] ) + 1 ) );
				Q_snprintfz( tempname, tempname_size, "%s/%s", search->path, filenames[i] );

				f = fopen( tempname, "rb" );
				if( f )
				{
					fclose( f );
					Mem_TempFree( filenames[0] );
					Mem_TempFree( filenames );
					return extensions[i];
				}
			}
		}
		if( !search->next && purepass )
		{
			search = fs_searchpaths;
			purepass = qfalse;
		}
		else
		{
			search = search->next;
		}
	}

	Mem_TempFree( filenames[0] );
	Mem_TempFree( filenames );

	return NULL;
}

/*
   ==============
   FS_FileExists
   ==============
 */
static int FS_FileExists( const char *filename )
{
	searchpath_t *search = FS_SearchPathForFile( filename );

	if( !search )
		return -1;

	if( search->pack )
	{
		pack_t *pak = search->pack;
		packfile_t *pakFile;
		unsigned hashKey = FS_PackHashKey( filename, pak->hashSize );

		for( pakFile = pak->filesHash[hashKey]; pakFile; pakFile = pakFile->hashNext )
		{
			if( !Q_stricmp( filename, pakFile->name ) )
				return pakFile->uncompressedSize;
		}
	}
	else
	{
		FILE *f;
		int pos, end;

		FS_CheckTempnameSize( sizeof( char ) * ( strlen( search->path ) + 1 + strlen( filename ) + 1 ) );
		Q_snprintfz( tempname, tempname_size, "%s/%s", search->path, filename );

		f = fopen( tempname, "rb" );
		if( f )
		{
			pos = ftell( f );
			fseek( f, 0, SEEK_END );
			end = ftell( f );
			fseek( f, pos, SEEK_SET );

			fclose( f );
			return end;
		}
	}

	return -1;
}

/*
   ==============
   FS_BaseFileExists
   ==============
 */
static int FS_BaseFileExists( const char *filename )
{
	FILE *f;
	int pos, end;
	searchpath_t *search;

	search = FS_SearchPathForBaseFile( filename );
	if( !search )
		return -1;
	assert( !search->pack );

	FS_CheckTempnameSize( sizeof( char ) * ( strlen( search->path ) + 1 + strlen( filename ) + 1 ) );
	Q_snprintfz( tempname, tempname_size, "%s/%s", search->path, filename );

	f = fopen( tempname, "rb" );
	if( !f )
		return -1;

	pos = ftell( f );
	fseek( f, 0, SEEK_END );
	end = ftell( f );
	fseek( f, pos, SEEK_SET );

	fclose( f );
	return end;
}

/*
   ==============
   FS_AbsoluteFileExists
   ==============
 */
int FS_AbsoluteFileExists( const char *filename )
{
	FILE *f;
	int pos, end;

	if( !COM_ValidateFilename( filename ) )
		return -1;

	f = fopen( filename, "rb" );
	if( !f )
		return -1;

	pos = ftell( f );
	fseek( f, 0, SEEK_END );
	end = ftell( f );
	fseek( f, pos, SEEK_SET );

	fclose( f );
	return end;
}

/*
   ===========
   FS_FOpenFile

   Finds the file in the search path. Returns filesize and an open handle
   Used for streaming data out of either a pak file or a separate file.
   ===========
 */
int FS_FOpenFile( const char *filename, int *filenum, int mode )
{
	FILE *f;
	searchpath_t *search;
	filehandle_t *file;
	int pos, end;

	assert( mode == FS_READ || mode == FS_WRITE || mode == FS_APPEND );
	assert( filenum || mode == FS_READ );

	if( filenum )
		*filenum = 0;

	if( !COM_ValidateRelativeFilename( filename ) )
		return -1;

	if( !filenum )
	{
		if( mode == FS_READ )
			return FS_FileExists( filename );
		return -1;
	}

	if( mode == FS_WRITE )
	{
		FS_CheckTempnameSize( strlen( FS_WriteDirectory() ) + 1 + strlen( FS_GameDirectory() ) + 1 + strlen( filename ) + 1 );
		Q_snprintfz( tempname, tempname_size, "%s/%s/%s", FS_WriteDirectory(), FS_GameDirectory(), filename );
		FS_CreateAbsolutePath( tempname );

		f = fopen( tempname, "wb" );
		if( !f )
			return -1;

		*filenum = FS_OpenFileHandle();
		file = &fs_filehandles[*filenum - 1];
		file->fstream = f;
		file->offset = 0;
		file->zipEntry = NULL;
		file->uncompressedSize = 0;
		file->restReadUncompressed = 0;

		return 0;
	}
	else if( mode == FS_APPEND )
	{
		FS_CheckTempnameSize( strlen( FS_WriteDirectory() ) + 1 + strlen( FS_GameDirectory() ) + 1 + strlen( filename ) + 1 );
		Q_snprintfz( tempname, tempname_size, "%s/%s/%s", FS_WriteDirectory(), FS_GameDirectory(), filename );
		FS_CreateAbsolutePath( tempname );

		f = fopen( tempname, "ab" );
		if( !f )
			return -1;

		pos = ftell( f );
		fseek( f, 0, SEEK_END );
		end = ftell( f );
		fseek( f, pos, SEEK_SET );

		*filenum = FS_OpenFileHandle();
		file = &fs_filehandles[*filenum - 1];
		file->fstream = f;
		file->zipEntry = NULL;
		file->offset = 0;
		file->uncompressedSize = end;
		file->restReadUncompressed = 0;
		return end;
	}
	else // FS_READ
	{
		search = FS_SearchPathForFile( filename );

		if( !search )
		{
			*filenum = 0;
			Com_DPrintf( "FS_FOpenFile: can't find %s\n", filename );
			return -1;
		}

		if( search->pack )
		{
			pack_t *pak = search->pack;
			packfile_t *pakFile;
			unsigned hashKey = FS_PackHashKey( filename, pak->hashSize );

			for( pakFile = pak->filesHash[hashKey]; pakFile; pakFile = pakFile->hashNext )
			{
				if( Q_stricmp( filename, pakFile->name ) )
					continue;

				*filenum = FS_OpenFileHandle();
				file = &fs_filehandles[*filenum - 1];
				file->fstream = fopen( pak->filename, "rb" );
				if( !file->fstream )
					Com_Error( ERR_FATAL, "Error opening pak file: %s", pak->filename );
				file->uncompressedSize = pakFile->uncompressedSize;
				file->restReadUncompressed = pakFile->uncompressedSize;
				file->zipEntry = NULL;

				if( !( pakFile->flags & FS_PACKFILE_COHERENT ) )
				{
					unsigned offset = FS_PK3CheckFileCoherency( file->fstream, pakFile );
					if( !offset )
					{
						Com_DPrintf( "FS_FOpenFile: can't get proper offset for %s\n", filename );
						FS_FCloseFile( *filenum );
						*filenum = 0;
						return -1;
					}
					pakFile->offset += offset;
					pakFile->flags |= FS_PACKFILE_COHERENT;
				}
				file->offset = pakFile->offset;

				if( pakFile->flags & FS_PACKFILE_DEFLATED )
				{
					if( fs_globalZipEntryUser )
						file->zipEntry = Mem_Alloc( fs_mempool, sizeof( zipEntry_t ) );
					else
					{
						// reset zstream here too, Mem_Alloc does it for us above
						memset( &fs_globalZipEntry.zstream, 0, sizeof( fs_globalZipEntry.zstream ) );

						file->zipEntry = &fs_globalZipEntry;
						fs_globalZipEntryUser = file;
					}

					file->zipEntry->compressedSize = pakFile->compressedSize;
					file->zipEntry->restReadCompressed = pakFile->compressedSize;

					// windowBits is passed < 0 to tell that there is no zlib header.
					// Note that in this case inflate *requires* an extra "dummy" byte
					// after the compressed stream in order to complete decompression and
					// return Z_STREAM_END. We don't want absolutely Z_STREAM_END because we known the
					// size of both compressed and uncompressed data
					if( inflateInit2( &file->zipEntry->zstream, -MAX_WBITS ) != Z_OK )
					{
						Com_DPrintf( "FS_FOpenFile: can't inflate %s\n", filename );
						FS_FCloseFile( *filenum );
						*filenum = 0;
						return -1;
					}
				}

				if( fseek( file->fstream, file->offset, SEEK_SET ) != 0 )
				{
					Com_DPrintf( "FS_FOpenFile: can't inflate %s\n", filename );
					FS_FCloseFile( *filenum );
					*filenum = 0;
					return -1;
				}

				Com_DPrintf( "PackFile: %s : %s\n", pak->filename, filename );
				return pakFile->uncompressedSize;
			}
		}
		else
		{
			FS_CheckTempnameSize( sizeof( char ) * ( strlen( search->path ) + 1 + strlen( filename ) + 1 ) );
			Q_snprintfz( tempname, tempname_size, "%s/%s", search->path, filename );

			f = fopen( tempname, "rb" );
			if( f )
			{
				pos = ftell( f );
				fseek( f, 0, SEEK_END );
				end = ftell( f );
				fseek( f, pos, SEEK_SET );

				*filenum = FS_OpenFileHandle();
				file = &fs_filehandles[*filenum - 1];
				file->offset = 0;
				file->zipEntry = NULL;
				file->fstream = f;
				file->uncompressedSize = end;
				file->restReadUncompressed = end;

				Com_DPrintf( "FS_FOpenFile: %s\n", tempname );
				return end;
			}
		}

		*filenum = 0;
		Com_DPrintf( "FS_FOpenFile: can't find %s\n", filename );
		return -1;
	}
}

/*
   ===========
   FS_FOpenBaseFile

   Same for base files, won't look inside paks.
   ===========
 */
int FS_FOpenBaseFile( const char *filename, int *filenum, int mode )
{
	FILE *f;
	searchpath_t *search;
	filehandle_t *file;
	int pos, end;

	assert( mode == FS_READ || mode == FS_WRITE || mode == FS_APPEND );
	assert( filenum || mode == FS_READ );

	if( !COM_ValidateRelativeFilename( filename ) )
		return -1;

	if( filenum )
		*filenum = 0;

	if( !filenum )
	{
		if( mode == FS_READ )
			return FS_BaseFileExists( filename );
		return -1;
	}

	if( mode == FS_WRITE )
	{
		FS_CheckTempnameSize( strlen( FS_WriteDirectory() ) + 1 + strlen( filename ) + 1 );
		Q_snprintfz( tempname, tempname_size, "%s/%s", FS_WriteDirectory(), filename );

		FS_CreateAbsolutePath( tempname );
		f = fopen( tempname, "wb" );
		if( !f )
			return -1;

		*filenum = FS_OpenFileHandle();
		file = &fs_filehandles[*filenum - 1];
		file->fstream = f;
		file->offset = 0;
		file->zipEntry = NULL;
		file->uncompressedSize = 0;
		file->restReadUncompressed = 0;

		return 0;
	}
	else if( mode == FS_APPEND )
	{
		FS_CheckTempnameSize( strlen( FS_WriteDirectory() ) + 1 + strlen( filename ) + 1 );
		Q_snprintfz( tempname, tempname_size, "%s/%s", FS_WriteDirectory(), filename );

		FS_CreateAbsolutePath( tempname );
		f = fopen( tempname, "ab" );
		if( !f )
			return -1;

		pos = ftell( f );
		fseek( f, 0, SEEK_END );
		end = ftell( f );
		fseek( f, pos, SEEK_SET );

		*filenum = FS_OpenFileHandle();
		file = &fs_filehandles[*filenum - 1];
		file->fstream = f;
		file->zipEntry = NULL;
		file->offset = 0;
		file->uncompressedSize = end;
		file->restReadUncompressed = 0;
		return end;
	}
	else // FS_READ
	{
		search = FS_SearchPathForBaseFile( filename );

		if( !search )
		{
			Com_DPrintf( "FS_FOpenBaseFile: can't find %s\n", filename );
			return -1;
		}
		assert( !search->pack );

		FS_CheckTempnameSize( sizeof( char ) * ( strlen( search->path ) + 1 + strlen( filename ) + 1 ) );
		Q_snprintfz( tempname, tempname_size, "%s/%s", search->path, filename );

		f = fopen( tempname, "rb" );
		if( !f )
		{
			Com_DPrintf( "FS_FOpenBaseFile: can't find %s\n", filename );
			return -1;
		}

		pos = ftell( f );
		fseek( f, 0, SEEK_END );
		end = ftell( f );
		fseek( f, pos, SEEK_SET );

		*filenum = FS_OpenFileHandle();
		file = &fs_filehandles[*filenum - 1];
		file->offset = 0;
		file->zipEntry = NULL;
		file->fstream = f;
		file->uncompressedSize = end;
		file->restReadUncompressed = end;

		Com_DPrintf( "FS_FOpenBaseFile: %s\n", tempname );
		return end;
	}
}

/*
   ===========
   FS_FOpenAbsoluteFile

   Same for absolute files, won't look inside paks.
   ===========
 */
int FS_FOpenAbsoluteFile( const char *filename, int *filenum, int mode )
{
	FILE *f;
	filehandle_t *file;
	int pos, end;

	assert( mode == FS_READ || mode == FS_WRITE || mode == FS_APPEND );
	assert( filenum || mode == FS_READ );

	if( !COM_ValidateFilename( filename ) )
		return -1;

	if( filenum )
		*filenum = 0;

	if( !filenum )
	{
		if( mode == FS_READ )
			return FS_AbsoluteFileExists( filename );
		return -1;
	}

	if( mode == FS_WRITE )
	{
		FS_CreateAbsolutePath( filename );

		f = fopen( filename, "wb" );
		if( !f )
			return -1;

		*filenum = FS_OpenFileHandle();
		file = &fs_filehandles[*filenum - 1];
		file->fstream = f;
		file->offset = 0;
		file->zipEntry = NULL;
		file->uncompressedSize = 0;
		file->restReadUncompressed = 0;

		return 0;
	}
	else if( mode == FS_APPEND )
	{
		FS_CreateAbsolutePath( filename );

		f = fopen( filename, "ab" );
		if( !f )
			return -1;

		pos = ftell( f );
		fseek( f, 0, SEEK_END );
		end = ftell( f );
		fseek( f, pos, SEEK_SET );

		*filenum = FS_OpenFileHandle();
		file = &fs_filehandles[*filenum - 1];
		file->fstream = f;
		file->zipEntry = NULL;
		file->offset = 0;
		file->uncompressedSize = end;
		file->restReadUncompressed = 0;
		return end;
	}
	else // FS_READ
	{
		f = fopen( filename, "rb" );
		if( !f )
		{
			Com_DPrintf( "FS_FOpenAbsoluteFile: can't find %s\n", filename );
			return -1;
		}

		pos = ftell( f );
		fseek( f, 0, SEEK_END );
		end = ftell( f );
		fseek( f, pos, SEEK_SET );

		*filenum = FS_OpenFileHandle();
		file = &fs_filehandles[*filenum - 1];
		file->offset = 0;
		file->zipEntry = NULL;
		file->fstream = f;
		file->uncompressedSize = end;
		file->restReadUncompressed = end;

		Com_DPrintf( "FS_FOpenAbsoluteFile: %s\n", filename );
		return end;
	}
}

/*
   ==============
   FS_FCloseFile
   ==============
 */
void FS_FCloseFile( int file )
{
	filehandle_t *fh;

	if( !file )
		return; // return silently

	fh = FS_FileHandleForNum( file );
	if( fh->zipEntry )
	{
		inflateEnd( &fh->zipEntry->zstream );
		if( fh != fs_globalZipEntryUser )
			Mem_Free( fh->zipEntry );
		else
			fs_globalZipEntryUser = NULL;
		fh->zipEntry = NULL;
	}
	if( fh->fstream )
	{
		fclose( fh->fstream );
		fh->fstream = NULL;
	}

	FS_CloseFileHandle( fh );
}

/*
   ==============
   FS_FCloseBaseFile
   ==============
 */
void FS_FCloseBaseFile( int file )
{
	FS_FCloseFile( file );
}

/*
   ==============
   FS_FCloseAbsoluteFile
   ==============
 */
static void FS_FCloseAbsoluteFile( int file )
{
	FS_FCloseFile( file );
}

/*
   =================
   FS_ReadPK3File

   Properly handles partial reads, used by FS_Read and FS_Seek
   =================
 */
static int FS_ReadPK3File( qbyte *buf, size_t len, filehandle_t *fh )
{
	zipEntry_t *zipEntry;
	size_t block, total;
	int read, error, totalOutBefore;

	zipEntry = fh->zipEntry;
	zipEntry->zstream.next_out = buf;
	zipEntry->zstream.avail_out = (unsigned int)len;

	total = 0;
	do
	{   // read in chunks
		if( !zipEntry->zstream.avail_in && zipEntry->restReadCompressed )
		{
			block = min( zipEntry->restReadCompressed, FS_ZIP_BUFSIZE );

			read = (int)fread( zipEntry->readBuffer, 1, block, fh->fstream );
			if( read == 0 )
			{       // we might have been trying to read from a CD
				read = (int)fread( zipEntry->readBuffer, 1, block, fh->fstream );
				if( read == 0 )
					read = -1;
			}
			if( read == -1 )
				Sys_Error( "FS_Read: can't read %i bytes", block );

			zipEntry->restReadCompressed -= (unsigned)block;
			zipEntry->zstream.next_in = zipEntry->readBuffer;
			zipEntry->zstream.avail_in = (unsigned)block;
		}

		totalOutBefore = zipEntry->zstream.total_out;
		error = inflate( &zipEntry->zstream, Z_SYNC_FLUSH );
		total += ( zipEntry->zstream.total_out - totalOutBefore );

		if( error == Z_STREAM_END )
			break;
		if( error != Z_OK )
			Sys_Error( "FS_ReadPK3File: can't inflate file" );
	}
	while( zipEntry->zstream.avail_out > 0 );

	return (int)total;
}

/*
   =================
   FS_ReadFile

   Properly handles partial reads
   =================
 */
static int FS_ReadFile( qbyte *buf, size_t len, filehandle_t *fh )
{
	size_t block, total;
	int read;

	total = 0;
	do
	{   // read in chunks
		block = min( len, FS_MAX_BLOCK_SIZE );

		read = (int)fread( buf, 1, block, fh->fstream );
		if( read == 0 )
		{           // we might have been trying to read from a CD
			read = (int)fread( buf, 1, block, fh->fstream );
			if( read == 0 )
				read = -1;
		}
		if( read == -1 )
			Sys_Error( "FS_Read: could not read %i bytes", block );

		// do some progress bar thing here...
		len -= block;
		buf += block;
		total += block;
	}
	while( len > 0 );

	return (int)total;
}

/*
   =================
   FS_Read

   Properly handles partial reads
   =================
 */
int FS_Read( void *buffer, size_t len, int file )
{
	filehandle_t *fh;
	size_t total;

	fh = FS_FileHandleForNum( file );

	// read in chunks for progress bar
	if( len > fh->restReadUncompressed )
		len = fh->restReadUncompressed;
	if( !len || !buffer )
		return 0;

	if( fh->zipEntry )
		total = FS_ReadPK3File( ( qbyte * )buffer, len, fh );
	else
		total = FS_ReadFile( ( qbyte * )buffer, len, fh );

	fh->restReadUncompressed -= (unsigned)total;

	return (int)total;
}

/*
   ============
   FS_Print
   ============
 */
int FS_Print( int file, const char *msg )
{
	return ( msg ? FS_Write( msg, strlen( msg ), file ) : 0 );
}

/*
   ============
   FS_Printf
   ============
 */
int FS_Printf( int file, const char *format, ... )
{
	char msg[MAX_PRINTMSG];
	size_t len;
	va_list	argptr;

	va_start( argptr, format );
	if( ( len = Q_vsnprintfz( msg, sizeof( msg ), format, argptr ) ) >= sizeof( msg )-1 )
		Sys_Error( "FS_Printf: Buffer overflow" );
	va_end( argptr );

	return FS_Write( msg, len, file );
}

/*
   ============
   FS_Write

   Properly handles partial writes
   ============
 */
int FS_Write( const void *buffer, size_t len, int file )
{
	int write;
	filehandle_t *fh;
	int block, remaining, total;
	qbyte *buf;

	fh = FS_FileHandleForNum( file );
	if( fh->zipEntry )
		Sys_Error( "FS_Write: writing to compressed file" );

	buf = ( qbyte * )buffer;
	total = 0;
	remaining = (int)len;
	if( !remaining || !buf )
		return 0;

	do
	{
		block = remaining;
		if( block > FS_MAX_BLOCK_SIZE )
			block = FS_MAX_BLOCK_SIZE;

		write = (int)fwrite( buf, 1, block, fh->fstream );
		if( write == 0 )
		{               // try once more
			write = (int)fwrite( buffer, 1, block, fh->fstream );
			if( write == 0 )
				write = -1;
		}
		if( write == -1 )
			Sys_Error( "FS_Read: can't write %i bytes", block );

		remaining -= block;
		buf += block;
		total += block;
	}
	while( remaining > 0 );

	fh->uncompressedSize += total;
	return total;
}

/*
   ============
   FS_Tell
   ============
 */
int FS_Tell( int file )
{
	filehandle_t *fh;

	fh = FS_FileHandleForNum( file );

	return fh->uncompressedSize - fh->restReadUncompressed;
}

/*
   ============
   FS_Tell
   ============
 */
int FS_Seek( int file, int offset, int whence )
{
	filehandle_t *fh;
	zipEntry_t *zipEntry;
	int error, currentOffset;
	size_t remaining, block;
	qbyte buf[FS_ZIP_BUFSIZE * 4];

	fh = FS_FileHandleForNum( file );
	currentOffset = fh->uncompressedSize - fh->restReadUncompressed;

	if( whence == FS_SEEK_CUR )
		offset += currentOffset;
	else if( whence == FS_SEEK_END )
		offset += fh->uncompressedSize;
	else if( whence != FS_SEEK_SET )
		return -1;

	// clamp so we don't get out of bounds
	clamp( offset, 0, (int)fh->uncompressedSize );
	if( offset == currentOffset )
		return 0;

	if( !fh->zipEntry )
	{
		fh->restReadUncompressed = fh->uncompressedSize - offset;
		return fseek( fh->fstream, fh->offset + offset, SEEK_SET );
	}

	// compressed files, doh
	zipEntry = fh->zipEntry;

	if( offset > currentOffset )
	{
		offset -= currentOffset;
	}
	else
	{
		if( fseek( fh->fstream, fh->offset, SEEK_SET ) != 0 )
			return -1;

		zipEntry->zstream.next_in = zipEntry->readBuffer;
		zipEntry->zstream.avail_in = 0;
		error = inflateReset( &zipEntry->zstream );
		if( error != Z_OK )
			Sys_Error( "FS_Seek: can't inflateReset file" );

		fh->restReadUncompressed = fh->uncompressedSize;
		zipEntry->restReadCompressed = zipEntry->compressedSize;
	}

	remaining = offset;
	do
	{
		block = min( remaining, sizeof( buf ) );

		FS_ReadPK3File( buf, block, fh );

		remaining -= block;
	}
	while( remaining > 0 );

	fh->restReadUncompressed -= offset;
	return 0;
}

/*
   ============
   FS_Eof
   ============
 */
int FS_Eof( int file )
{
	filehandle_t *fh;

	fh = FS_FileHandleForNum( file );

	return !fh->restReadUncompressed;
}

/*
   ============
   FS_FFlush
   ============
 */
int FS_Flush( int file )
{
	filehandle_t *fh;

	fh = FS_FileHandleForNum( file );

	return fflush( fh->fstream );
}

/*
   ============
   FS_LoadFileExt

   Filename are relative to the quake search path
   a null buffer will just return the file length without loading
   ============
 */
int FS_LoadFileExt( const char *path, void **buffer, void *stack, size_t stackSize, const char *filename, int fileline )
{
	qbyte *buf;
	unsigned int len;
	int fhandle;

	buf = NULL; // quiet compiler warning

	// look for it in the filesystem or pack files
	len = FS_FOpenFile( path, &fhandle, FS_READ );
	if( !fhandle )
	{
		if( buffer )
			*buffer = NULL;
		return -1;
	}

	if( !buffer )
	{
		FS_FCloseFile( fhandle );
		return len;
	}

	if( stack && ( stackSize > len ) )
		buf = stack;
	else
		buf = _Mem_AllocExt( tempMemPool, len + 1, 0, 0, 0, filename, fileline );
	buf[len] = 0;
	*buffer = buf;

	FS_Read( buf, len, fhandle );
	FS_FCloseFile( fhandle );

	return len;
}

/*
   ============
   FS_LoadBaseFileExt

   a NULL buffer will just return the file length without loading
   ============
 */
int FS_LoadBaseFileExt( const char *path, void **buffer, void *stack, size_t stackSize, const char *filename, int fileline )
{
	qbyte *buf;
	unsigned int len;
	int fhandle;

	buf = NULL; // quiet compiler warning

	// look for it in the filesystem or pack files
	len = FS_FOpenBaseFile( path, &fhandle, FS_READ );
	if( !fhandle )
	{
		if( buffer )
			*buffer = NULL;
		return -1;
	}

	if( !buffer )
	{
		FS_FCloseBaseFile( fhandle );
		return len;
	}

	if( stack && ( stackSize > len ) )
		buf = stack;
	else
		buf = _Mem_AllocExt( tempMemPool, len + 1, 0, 0, 0, filename, fileline );
	buf[len] = 0;
	*buffer = buf;

	FS_Read( buf, len, fhandle );
	FS_FCloseBaseFile( fhandle );

	return len;
}

/*
   =============
   FS_FreeFile
   =============
 */
void FS_FreeFile( void *buffer )
{
	Mem_TempFree( buffer );
}

/*
   =============
   FS_FreeBaseFile
   =============
 */
void FS_FreeBaseFile( void *buffer )
{
	FS_FreeFile( buffer );
}

#define MD5_MAGIC( digest )   ( ( digest )[0] << 24 | ( digest )[1] << 16 | ( digest )[2] << 8 | ( digest )[3] ) ^ \
        ( ( digest )[4] << 24 | ( digest )[5] << 16 | ( digest )[6] << 8 | ( digest )[7] ) ^ \
        ( ( digest )[8] << 24 | ( digest )[9] << 16 | ( digest )[10] << 8 | ( digest )[11] ) ^ \
        ( ( digest )[12] << 24 | ( digest )[13] << 16 | ( digest )[14] << 8 | ( digest )[15] )

/*
   ============
   FS_ChecksumAbsoluteFile
   ============
 */
unsigned FS_ChecksumAbsoluteFile( const char *filename )
{
	qbyte buffer[FS_MAX_BLOCK_SIZE];
	int left, length, filenum;
	md5_byte_t digest[16];
	md5_state_t state;

	Com_DPrintf( "Calculating checksum for file: %s\n", filename );

	md5_init( &state );

	left = FS_FOpenAbsoluteFile( filename, &filenum, FS_READ );
	if( left == -1 )
		return 0;

	while( ( length = FS_Read( buffer, sizeof( buffer ), filenum ) ) )
	{
		left -= length;
		md5_append( &state, (md5_byte_t *)buffer, length );
	}

	FS_FCloseAbsoluteFile( filenum );
	md5_finish( &state, digest );

	if( left != 0 )
		return 0;

	return MD5_MAGIC( digest );
}

/*
   ============
   FS_ChecksumPK3File
   ============
 */
static unsigned FS_ChecksumPK3File( const char *filename, int numPakFiles, int *checksums )
{
	md5_byte_t digest[16];
	md5_state_t state;

	Com_DPrintf( "Calculating checksum for file: %s\n", filename );

	md5_init( &state );
	md5_append( &state, (md5_byte_t *)checksums, sizeof( *checksums ) * numPakFiles );
	md5_finish( &state, digest );

	return MD5_MAGIC( digest );
}

/*
   ============
   FS_PakChecksum
   ============
 */
static unsigned FS_PakChecksum( const char *filename )
{
	int diff;
	searchpath_t *search;

	for( search = fs_searchpaths; search; search = search->next )
	{
		if( search->pack )
		{
			// filename is a basename, so we only compare the end of the names
			diff = strlen( search->pack->filename ) - strlen( filename );
			if( diff >= 0 && !strcmp( search->pack->filename+diff, filename ) )
				return search->pack->checksum;
		}
	}

	return 0;
}

/*
   ================
   FS_ChecksumBaseFile
   ================
 */
unsigned FS_ChecksumBaseFile( const char *filename )
{
	const char *fullname;

	if( !Q_stricmp( COM_FileExtension( filename ), ".pk3" ) )
		return FS_PakChecksum( filename );

	fullname = FS_AbsoluteNameForBaseFile( filename );
	if( !fullname )
		return qfalse;

	return FS_ChecksumAbsoluteFile( fullname );
}

/*
   ============
   FS_AddPurePak
   ============
 */
qboolean FS_AddPurePak( unsigned checksum )
{
	searchpath_t *search;

	for( search = fs_searchpaths; search; search = search->next )
	{
		if( search->pack && search->pack->checksum == checksum )
		{
			search->pack->pure = qtrue;
			return qtrue;
		}
	}

	return qfalse;
}

/*
   ============
   FS_RemovePurePaks
   ============
 */
void FS_RemovePurePaks( void )
{
	searchpath_t *search;

	for( search = fs_searchpaths; search; search = search->next )
	{
		if( search->pack )
			search->pack->pure = qfalse;
	}
}

/*
   ============
   FS_IsPureFile
   ============
 */
qboolean FS_IsPureFile( const char *filename )
{
	searchpath_t *search = FS_SearchPathForFile( filename );

	if( !search || !search->pack )
		return qfalse;

	return search->pack->pure;
}

/*
   ================
   FS_CopyFile
   ================
 */
qboolean FS_CopyFile( const char *src, const char *dst )
{
	int srcnum, dstnum, length, l;
	qbyte buffer[FS_MAX_BLOCK_SIZE];

	if( FS_FOpenFile( dst, &dstnum, FS_WRITE ) == -1 )
		return qfalse;

	length = FS_FOpenFile( src, &srcnum, FS_READ );

	if( length == -1 )
	{
		FS_FCloseFile( dstnum );
		return qfalse;
	}

	while( qtrue )
	{
		l = FS_Read( buffer, sizeof( buffer ), srcnum );
		if( !l )
			break;
		FS_Write( buffer, l, dstnum );
		length -= l;
	}

	FS_FCloseFile( dstnum );
	FS_FCloseFile( srcnum );

	if( length != 0 )
	{
		FS_RemoveFile( dst );
		return qfalse;
	}

	return qtrue;
}

/*
   ================
   FS_CopyBaseFile
   ================
 */
qboolean FS_CopyBaseFile( const char *src, const char *dst )
{
	int srcnum, dstnum, left, length;
	qbyte buffer[FS_MAX_BLOCK_SIZE];

	if( ( left = FS_FOpenBaseFile( src, &srcnum, FS_READ ) ) == -1 )
		return qfalse;

	if( FS_FOpenBaseFile( dst, &dstnum, FS_WRITE ) == -1 )
	{
		FS_FCloseBaseFile( srcnum );
		return qfalse;
	}

	while( qtrue && left >= 0 )
	{
		length = FS_Read( buffer, sizeof( buffer ), srcnum );
		if( !length )
			break;
		FS_Write( buffer, length, dstnum );
		left -= length;
	}

	FS_FCloseBaseFile( dstnum );
	FS_FCloseBaseFile( srcnum );

	if( left != 0 )
	{
		FS_RemoveBaseFile( dst );
		return qfalse;
	}

	return qtrue;
}

/*
   ================
   FS_MoveFile
   ================
 */
qboolean FS_MoveFile( const char *src, const char *dst )
{
	const char *fullname;

	fullname = FS_AbsoluteNameForFile( src );

	if( !fullname )
		return qfalse;

	if( strncmp( fullname, FS_WriteDirectory(), strlen( FS_WriteDirectory() ) ) )
		return qfalse;

	if( !COM_ValidateRelativeFilename( dst ) )
		return qfalse;

	return ( !rename( fullname, va( "%s/%s/%s", FS_WriteDirectory(), FS_GameDirectory(), dst ) ) );
}

/*
   ================
   FS_MoveBaseFile
   ================
 */
qboolean FS_MoveBaseFile( const char *src, const char *dst )
{
	const char *fullname;

	fullname = FS_AbsoluteNameForBaseFile( src );

	if( !fullname )
		return qfalse;

	if( strncmp( fullname, FS_WriteDirectory(), strlen( FS_WriteDirectory() ) ) )
		return qfalse;

	if( !COM_ValidateRelativeFilename( dst ) )
		return qfalse;

	return ( !rename( fullname, va( "%s/%s", FS_WriteDirectory(), dst ) ) );
}

/*
   ================
   FS_RemoveAbsoluteFile
   ================
 */
qboolean FS_RemoveAbsoluteFile( const char *filename )
{
	if( !COM_ValidateFilename( filename ) )
		return qfalse;

	return ( !remove( filename ) );
}

/*
   ================
   FS_RemoveBaseFile
   ================
 */
qboolean FS_RemoveBaseFile( const char *filename )
{
	const char *fullname = FS_AbsoluteNameForBaseFile( filename );

	if( !fullname )
		return qfalse;

	if( strncmp( fullname, FS_WriteDirectory(), strlen( FS_WriteDirectory() ) ) )
		return qfalse;

	return ( FS_RemoveAbsoluteFile( fullname ) );
}

/*
   ================
   FS_RemoveFile
   ================
 */
qboolean FS_RemoveFile( const char *filename )
{
	const char *fullname = FS_AbsoluteNameForFile( filename );

	if( !fullname )
		return qfalse;

	if( strncmp( fullname, FS_WriteDirectory(), strlen( FS_WriteDirectory() ) ) )
		return qfalse;

	return ( FS_RemoveAbsoluteFile( fullname ) );
}

/*
   ================
   FS_RemoveAbsoluteDirectory
   ================
 */
qboolean FS_RemoveAbsoluteDirectory( const char *dirname )
{
	if( !COM_ValidateFilename( dirname ) )
		return qfalse;

	return ( Sys_FS_RemoveDirectory( dirname ) );
}

/*
   ================
   FS_RemoveBaseDirectory
   ================
 */
qboolean FS_RemoveBaseDirectory( const char *dirname )
{
	if( !COM_ValidateRelativeFilename( dirname ) )
		return qfalse;

	return ( FS_RemoveAbsoluteDirectory( va( "%s/%s", FS_WriteDirectory(), dirname ) ) );
}

/*
   ================
   FS_RemoveDirectory
   ================
 */
qboolean FS_RemoveDirectory( const char *dirname )
{
	if( !COM_ValidateRelativeFilename( dirname ) )
		return qfalse;

	return ( FS_RemoveAbsoluteDirectory( va( "%s/%s/%s", FS_WriteDirectory(), FS_GameDirectory(), dirname ) ) );
}

/*
   =================
   FS_PK3SearchCentralDir

   Locate the central directory of a zipfile (at the end, just before the global comment)
   =================
 */
static unsigned FS_PK3SearchCentralDir( FILE *fin )
{
	unsigned fileSize, backRead;
	unsigned maxBack = 0xffff; // maximum size of global comment
	unsigned char buf[FS_ZIP_BUFREADCOMMENT+4];

	if( fseek( fin, 0, SEEK_END ) != 0 )
		return 0;

	fileSize = ftell( fin );
	if( maxBack > fileSize )
		maxBack = fileSize;

	backRead = 4;
	while( backRead < maxBack )
	{
		unsigned i, readSize, readPos;

		if( backRead + FS_ZIP_BUFREADCOMMENT > maxBack )
			backRead = maxBack;
		else
			backRead += FS_ZIP_BUFREADCOMMENT;

		readPos = fileSize - backRead;
		readSize = min( FS_ZIP_BUFREADCOMMENT + 4, backRead );
		if( readSize < 4 )
			continue;

		if( fseek( fin, readPos, SEEK_SET ) != 0 )
			break;
		if( fread( buf, 1, readSize, fin ) != readSize )
			break;

		for( i = readSize - 3; i--; )
		{
			// check the magic
			if( LittleLongRaw( buf + i ) == FS_ZIP_ENDHEADERMAGIC )
				return readPos + i;
		}
	}

	return 0;
}

/*
   =================
   FS_PK3GetFileInfo

   Get Info about the current file in the zipfile, with internal only info
   =================
 */
static unsigned FS_PK3GetFileInfo( FILE *f, unsigned pos, unsigned byteBeforeTheZipFile, packfile_t *file, size_t *fileNameLen, int *crc )
{
	size_t sizeRead;
	unsigned compressed;
	unsigned char infoHeader[46]; // we can't use a struct here because of packing

	if( fseek( f, pos, SEEK_SET ) != 0 )
		return 0;
	if( fread( infoHeader, 1, sizeof( infoHeader ), f ) != sizeof( infoHeader ) )
		return 0;

	// check the magic
	if( LittleLongRaw( &infoHeader[0] ) != FS_ZIP_CENTRALHEADERMAGIC )
		return 0;

	compressed = LittleShortRaw( &infoHeader[10] );
	if( compressed && ( compressed != Z_DEFLATED ) )
		return 0;

	if( crc )
		*crc = LittleLongRaw( &infoHeader[16] );
	if( file )
	{
		if( compressed == Z_DEFLATED )
			file->flags |= FS_PACKFILE_DEFLATED;
		file->compressedSize = LittleLongRaw( &infoHeader[20] );
		file->uncompressedSize = LittleLongRaw( &infoHeader[24] );
		file->offset = LittleLongRaw( &infoHeader[42] ) + byteBeforeTheZipFile;
	}

	sizeRead = ( size_t )LittleShortRaw( &infoHeader[28] );
	if( !sizeRead )
		return 0;

	if( fileNameLen )
		*fileNameLen = sizeRead;

	if( file )
	{
		if( fread( file->name, 1, sizeRead, f ) != sizeRead )
			return 0;
		*( file->name + sizeRead ) = 0;
	}

	return FS_ZIP_SIZECENTRALDIRITEM + ( unsigned )LittleShortRaw( &infoHeader[28] ) +
	       ( unsigned )LittleShortRaw( &infoHeader[30] ) + ( unsigned )LittleShortRaw( &infoHeader[32] );
}

/*
   =================
   FS_LoadPK3File

   Takes an explicit (not game tree related) path to a pak file.

   Loads the header and directory, adding the files at the beginning
   of the list so they override previous pack files.
   =================
 */
static pack_t *FS_LoadPK3File( const char *packfilename, qboolean silent )
{
	int i, hashSize;
	int *checksums = NULL;
	int numFiles;
	size_t namesLen, len;
	pack_t *pack = NULL;
	packfile_t *file;
	FILE *fin = NULL;
	char *names;
	unsigned hashKey;
	unsigned char zipHeader[20]; // we can't use a struct here because of packing
	unsigned offset, centralPos, sizeCentralDir, offsetCentralDir, byteBeforeTheZipFile;
	qboolean modulepack;
	const char *ext;
	void *handle = NULL;

	// lock the file for reading, but don't throw fatal error
	handle = Sys_FS_LockFile( packfilename );
	if( handle == NULL )
	{
		if( !silent ) Com_Printf( "Error locking PK3 file: %s\n", packfilename );
		goto error;
	}

	fin = fopen( packfilename, "rb" );
	if( fin == NULL )
	{
		if( !silent ) Com_Printf( "Error opening PK3 file: %s\n", packfilename );
		goto error;
	}
	centralPos = FS_PK3SearchCentralDir( fin );
	if( centralPos == 0 )
	{
		if( !silent ) Com_Printf( "No central directory found for PK3 file: %s\n", packfilename );
		goto error;
	}
	if( fseek( fin, centralPos, SEEK_SET ) != 0 )
	{
		if( !silent ) Com_Printf( "Error seeking PK3 file: %s\n", packfilename );
		goto error;
	}
	if( fread( zipHeader, 1, sizeof( zipHeader ), fin ) != sizeof( zipHeader ) )
	{
		if( !silent ) Com_Printf( "Error reading PK3 file: %s\n", packfilename );
		goto error;
	}

	// total number of entries in the central dir on this disk
	numFiles = LittleShortRaw( &zipHeader[8] );
	if( !numFiles )
	{
		if( !silent ) Com_Printf( "%s is not a valid pk3 file\n", packfilename );
		goto error;
	}
	if( LittleShortRaw( &zipHeader[10] ) != numFiles || LittleShortRaw( &zipHeader[6] ) != 0
	    || LittleShortRaw( &zipHeader[4] ) != 0 )
	{
		if( !silent ) Com_Printf( "%s is not a valid pk3 file\n", packfilename );
		goto error;
	}

	// size of the central directory
	sizeCentralDir = LittleLongRaw( &zipHeader[12] );

	// offset of start of central directory with respect to the starting disk number
	offsetCentralDir = LittleLongRaw( &zipHeader[16] );
	if( centralPos < offsetCentralDir + sizeCentralDir )
	{
		if( !silent ) Com_Printf( "%s is not a valid pk3 file\n", packfilename );
		goto error;
	}
	byteBeforeTheZipFile = centralPos - offsetCentralDir - sizeCentralDir;

	for( hashSize = 1; ( hashSize <= numFiles ) && ( hashSize < FS_MAX_HASH_SIZE ); hashSize <<= 1 ) ;

	for( i = 0, namesLen = 0, centralPos = offsetCentralDir + byteBeforeTheZipFile; i < numFiles; i++, centralPos += offset )
	{
		offset = FS_PK3GetFileInfo( fin, centralPos, byteBeforeTheZipFile, NULL, &len, NULL );
		if( !offset )
		{
			if( !silent ) Com_Printf( "%s is not a valid pk3 file\n", packfilename );
			goto error; // something wrong occured
		}
		namesLen += len + 1;
	}

	namesLen += 1; // add space for a guard

	pack = FS_Malloc( (int)( sizeof( pack_t ) + numFiles * sizeof( packfile_t ) + namesLen + hashSize * sizeof( packfile_t * ) ) );
	pack->filename = FS_CopyString( packfilename );
	pack->files = ( packfile_t * )( ( qbyte * )pack + sizeof( pack_t ) );
	pack->fileNames = names = ( char * )( ( qbyte * )pack->files + numFiles * sizeof( packfile_t ) );
	pack->filesHash = ( packfile_t ** )( ( qbyte * )names + namesLen );
	pack->numFiles = numFiles;
	pack->hashSize = hashSize;
	pack->sysHandle = handle;

	// allocate temp memory for files' checksums
	checksums = Mem_TempMallocExt( ( numFiles + 1 ) * sizeof( *checksums ), 0 );

	if( !Q_strnicmp( COM_FileBase( packfilename ), "modules", strlen( "modules" ) ) )
		modulepack = qtrue;
	else
		modulepack = qfalse;

	// add all files to hash table
	for( i = 0, file = pack->files, centralPos = offsetCentralDir + byteBeforeTheZipFile; i < numFiles; i++, file++, centralPos += offset, names += len + 1 )
	{
		file->name = names;

		offset = FS_PK3GetFileInfo( fin, centralPos, byteBeforeTheZipFile, file, &len, &checksums[i] );

		if( !COM_ValidateRelativeFilename( file->name ) )
		{
			if( !silent ) Com_Printf( "%s contains filename that's not allowed: %s\n", packfilename, file->name );
			goto error;
		}

		ext = COM_FileExtension( file->name );
		if( modulepack )
		{
			if( !ext || ( Q_stricmp( ext, ".so" ) && Q_stricmp( ext, ".dll" ) &&
			             Q_stricmp( ext, ".txt" ) && Q_stricmp( ext, ".dylib" ) ) )
			{
				if( !silent )
					Com_Printf( "%s is a module pack, but includes non module file: %s\n", packfilename, file->name );
				goto error;
			}
		}
		else
		{
			if( ext && ( !Q_stricmp( ext, ".so" ) || !Q_stricmp( ext, ".dll" ) || !Q_stricmp( ext, ".dylib" ) ) )
			{
				if( !silent )
					Com_Printf( "%s is not module pack, but includes module file:\n", packfilename, file->name );
				goto error;
			}
		}

		hashKey = FS_PackHashKey( file->name, hashSize );
		file->hashNext = pack->filesHash[hashKey];
		pack->filesHash[hashKey] = file;
	}

	fclose( fin );
	fin = NULL;

	checksums[numFiles] = 0x1234567; // add some pseudo-random stuff
	pack->checksum = FS_ChecksumPK3File( pack->filename, numFiles + 1, checksums );

	if( !pack->checksum )
	{
		if( !silent ) Com_Printf( "Couldn't generate checksum for pk3 file: %s\n", packfilename );
		goto error;
	}

	Mem_TempFree( checksums );

	if( !silent ) Com_Printf( "Added pk3 file %s (%i files)\n", pack->filename, pack->numFiles, pack->checksum );

	return pack;

error:
	if( fin )
		fclose( fin );
	if( pack )
		FS_Free( pack );
	if( checksums )
		Mem_TempFree( checksums );
	if( handle != NULL )
		Sys_FS_UnlockFile( handle );

	return NULL;
}

// wsw : Medar
/*
   =================
   FS_AddPakFile

   Loads a PAK and adds it to the tree after it has been initialized. Used for downloads
   =================
 */
void FS_AddPakFile( const char *pakname )
{
	pack_t *pak;
	searchpath_t *new, *search, *prev;
	const char *fullname;
	int path_size;
	qboolean founddir;

	fullname = FS_AbsoluteNameForBaseFile( pakname );
	if( !fullname )
		return;

	pak = FS_LoadPK3File( fullname, qfalse );
	if( !pak )
		return;

	new = FS_Malloc( sizeof( searchpath_t ) );
	new->pack = pak;

	path_size = sizeof( char ) * ( COM_FilePathLength( fullname ) + 1 );
	new->path = FS_Malloc( path_size );
	Q_strncpyz( new->path, fullname, path_size );

	// find the right position
	founddir = qfalse;

	prev = NULL;
	search = fs_searchpaths;
	while( search )
	{
		if( !strcmp( search->path, new->path ) )
		{
			if( search->pack && Q_stricmp( COM_FileBase( search->pack->filename ), COM_FileBase( new->pack->filename ) ) < 0 )
				break;
			if( !founddir )
				founddir = qtrue;
		}
		else if( founddir )
		{
			break;
		}

		prev = search;
		search = search->next;
	}

	if( !prev )
	{
		new->next = fs_searchpaths;
		fs_searchpaths = new;
	}
	else
	{
		prev->next = new;
		new->next = search;
	}
}

/*
   ============
   FS_IsPakValid
   ============
 */
qboolean FS_IsPakValid( const char *filename, unsigned *checksum )
{
	const char *fullname = FS_AbsoluteNameForBaseFile( filename );
	pack_t *pakfile;

	if( !fullname )
		return qfalse;

	pakfile = FS_LoadPK3File( fullname, qtrue );
	if( pakfile )
	{               // unlock and free, we don't need this file anyway
		if( checksum )
			*checksum = pakfile->checksum;
		if( pakfile->sysHandle )
			Sys_FS_UnlockFile( pakfile->sysHandle );
		FS_Free( pakfile );
		return qtrue;
	}

	return qfalse;
}

/*
   =================
   FS_PathGetFileListExt
   =================
 */
static int FS_PathGetFileListExt( searchpath_t *search, const char *dir, const char *extension, searchfile_t *files, size_t size )
{
	int i;
	unsigned found;
	char *p, *s, *name;
	size_t dirlen, extlen, tokenlen;

	assert( search );
	assert( !dir || dir[strlen( dir )-1] != '/' );
	assert( files );
	assert( size );

	if( !search || ( dir && dir[strlen( dir )-1] == '/' ) || !files || !size )
		return 0;

	found = 0;
	dirlen = 0;
	extlen = 0;

	if( dir )
		dirlen = strlen( dir );

	if( extension /* && extension[0] != '/'*/ )
		extlen = strlen( extension );
	else
		extlen = strlen( "*.*" );

	if( !search->pack )
	{
		size_t searchlen;
		int numfiles;
		char **filenames;
		unsigned int musthave, canthave;

		musthave = 0;
		canthave = SFF_HIDDEN | SFF_SYSTEM;

		FS_CheckTempnameSize( sizeof( char ) * ( strlen( search->path ) + 1 + dirlen + 1 + 1 /* asterisk */ + extlen + 1 ) );
		Q_strncpyz( tempname, search->path, tempname_size );
		Q_strncatz( tempname, "/", tempname_size );

		if( dirlen )
		{
			Q_strncatz( tempname, dir, tempname_size );
			Q_strncatz( tempname, "/", tempname_size );
		}
		searchlen = strlen( tempname );

		if( extension )
		{
			if( extension[0] != '/' )
			{
				Q_strncatz( tempname, "*", tempname_size );
				Q_strncatz( tempname, extension, tempname_size );
				canthave |= SFF_SUBDIR;
			}
			else
			{
				Q_strncatz( tempname, "*.*", tempname_size );
				musthave |= SFF_SUBDIR;
			}
		}
		else
		{
			Q_strncatz( tempname, "*.*", tempname_size );
		}

		if( ( filenames = FS_ListFiles( tempname, &numfiles, musthave, canthave ) ) )
		{
			for( i = 0; i < numfiles - 1; i++ )
			{
				if( found < size )
				{
					size_t len = strlen( filenames[i] + searchlen );

					if( ( musthave & SFF_SUBDIR ) )
					{
						if( filenames[i][searchlen+len-1] != '/' )
						{
							files[found].name = Mem_ZoneMalloc( len + 2 );
							strcpy( files[found].name, filenames[i] + searchlen );
							files[found].name[len] = '/';
							files[found].name[len+1] = 0;
						}
						else
						{
							files[found].name = ZoneCopyString( filenames[i] + searchlen );
						}
					}
					else
					{
						if( extension && ( len <= extlen ) )
						{
							Mem_ZoneFree( filenames[i] );
							continue;
						}
						files[found].name = ZoneCopyString( filenames[i] + searchlen );
					}
					files[found].searchPath = search;
					found++;
				}
				Mem_ZoneFree( filenames[i] );
			}
		}
		Mem_ZoneFree( filenames );

		return found;
	}

	for( s = search->pack->fileNames; *s; s += tokenlen + 1 )
	{
		tokenlen = strlen( s );

		// check directory
		if( dirlen )
		{
			if( tokenlen <= dirlen + 1 || s[dirlen] != '/' || Q_strnicmp( s, dir, dirlen ) )
				continue;
		}

		// check extension
		if( extension /* && (extension[0] != '/')*/ )
		{
			if( tokenlen <= extlen || Q_strnicmp( extension, s + tokenlen - extlen, extlen ) )
				continue;
		}

		if( dirlen )
			name = s + dirlen + 1;
		else
			name = s;


		// ignore subdirectories
		p = strchr( name, '/' );
		if( p )
		{
			if( *( p + 1 ) )
				continue;
		}

		files[found].name = name;
		files[found].searchPath = search;
		if( ++found == size )
			return found;
	}

	return found;
}

/*
   ================
   FS_GetFileListExt_
   ================
 */
#define FS_MIN_SEARCHFILES      0x400
#define FS_MAX_SEARCHFILES      0xFFFF          // cap
static int FS_SortFilesCmp( const searchfile_t *file1, const searchfile_t *file2 )
{
	return Q_stricmp( ( file1 )->name, ( file2 )->name );
}

static int FS_GetFileListExt_( const char *dir, const char *extension, char *buf, size_t *bufsize, int maxFiles, int start, int end )
{
	int i;
	int allfound = 0, found, limit;
	size_t len, alllen;
	searchpath_t *search;
	searchfile_t *files;
	qboolean purepass;
	static int maxFilesCache;
	static char dircache[MAX_QPATH], extcache[MAX_QPATH];
	qboolean useCache;

	assert( !dir || dir[strlen( dir )-1] != '/' );

	if( ( dir && dir[strlen( dir )-1] == '/' ) || !bufsize )
		return 0;

	if( fs_cursearchfiles )
	{
		useCache = ( maxFilesCache == maxFiles );
		if( useCache )
		{
			useCache = dir ? !strcmp( dircache, dir ) : ( dircache[0] == '\0' );
			if( useCache )
				useCache = extension ? !strcmp( extcache, extension ) : ( extcache[0] == '\0' );
		}
	}
	else
	{
		useCache = qfalse;
	}

	maxFilesCache = maxFiles;
	if( dir )
		Q_strncpyz( dircache, dir, sizeof( dircache ) );
	else
		dircache[0] = '\0';
	if( extension )
		Q_strncpyz( extcache, extension, sizeof( extcache ) );
	else
		extcache[0] = '\0';

	files = fs_searchfiles;
	if( !useCache )
	{
		search = fs_searchpaths;
		purepass = qtrue;
		while( search )
		{
			if( ( search->pack && search->pack->pure == purepass ) || ( !search->pack && !purepass ) )
			{
				limit = maxFiles ? min( fs_numsearchfiles, maxFiles ) : fs_numsearchfiles;
				found = FS_PathGetFileListExt( search, dir, extension, files + allfound,
				                               fs_numsearchfiles - allfound );

				if( allfound+found == fs_numsearchfiles )
				{
					if( limit == maxFiles || fs_numsearchfiles == FS_MAX_SEARCHFILES )
						break; // we are done
					fs_numsearchfiles *= 2;
					if( fs_numsearchfiles > FS_MAX_SEARCHFILES )
						fs_numsearchfiles = FS_MAX_SEARCHFILES;
					fs_searchfiles = files = FS_Realloc( fs_searchfiles, sizeof( searchfile_t ) * fs_numsearchfiles );
					if( !search->pack )
					{
						for( i = 0; i < found; i++ )
							Mem_ZoneFree( files[allfound+i].name );
					}
					continue;
				}

				allfound += found;
			}

			if( !search->next && purepass )
			{
				search = fs_searchpaths;
				purepass = qfalse;
			}
			else
			{
				search = search->next;
			}
		}

		qsort( files, allfound, sizeof( searchfile_t ), ( int ( * )( const void *, const void * ) )FS_SortFilesCmp );

		// remove all duplicates
		for( i = 1; i < allfound; ) 
		{
			if( FS_SortFilesCmp( &files[i-1], &files[i] ) )
			{
				i++;
				continue;
			}

			if( !files[i-1].searchPath->pack )
				Mem_ZoneFree( files[i-1].name );
			memmove( &files[i-1], &files[i], (allfound - i) * sizeof( searchfile_t ) );
			allfound--;
		}
	}

	if( !useCache )
		fs_cursearchfiles = allfound;
	else
		allfound = fs_cursearchfiles;

	if( start < 0 )
		start = 0;
	if( !end )
		end = allfound;
	else if( end > allfound )
		end = allfound;

	if( bufsize )
	{
		found = 0;

		if( buf )
		{
			alllen = 0;
			for( i = start; i < end; i++ )
			{
				len = strlen( files[i].name );
				if( *bufsize <= len + alllen )
					break; // we are done
				strcpy( buf + alllen, files[i].name );
				alllen += len + 1;
				found++;
			}
		}
		else
		{
			*bufsize = 0;
			for( i = start; i < end; found++, i++ )
				*bufsize += strlen( files[i].name ) + 1;
		}

		return found;
	}

	return allfound;
}

/*
   ================
   FS_GetFileList
   ================
 */
int FS_GetFileListExt( const char *dir, const char *extension, char *buf, size_t *bufsize, int start, int end )
{
	//	return FS_GetFileListExt_( dir, extension, buf, bufsize, buf2, buf2size, 0, 0, 0 );		// 0 - no limit
	return FS_GetFileListExt_( dir, extension, buf, bufsize, FS_MAX_SEARCHFILES, start, end );
}

/*
   ================
   FS_GetFileList
   ================
 */
int FS_GetFileList( const char *dir, const char *extension, char *buf, size_t bufsize, int start, int end )
{
	//	return FS_GetFileListExt_( dir, extension, buf, &bufsize, 0, start, end );				// 0 - no limit
	return FS_GetFileListExt_( dir, extension, buf, &bufsize, FS_MAX_SEARCHFILES, start, end );
}

/*
   ============
   FS_GameDirectory

   Returns the current game directory, without the path
   ============
 */
const char *FS_GameDirectory( void )
{
	assert( fs_game && fs_game->string && fs_game->string[0] );
	return fs_game->string;
}

/*
   ============
   FS_BaseGameDirectory

   Returns the current base game directory, without the path
   ============
 */
const char *FS_BaseGameDirectory( void )
{
	assert( fs_basegame && fs_basegame->string && fs_basegame->string[0] );
	return fs_basegame->string;
}

/*
   ============
   FS_WriteDirectory

   Returns directory where we can write, no gamedir attached
   ============
 */
const char *FS_WriteDirectory( void )
{
	return fs_basepaths->path;
}

/*
   ============
   FS_Path_f
   ============
 */
static void FS_Path_f( void )
{
	searchpath_t *s;

	Com_Printf( "Current search path:\n" );

	if( fs_searchpaths != fs_base_searchpaths )
		Com_Printf( "Mod files:\n" );
	for( s = fs_searchpaths; s; s = s->next )
	{
		if( s == fs_base_searchpaths )
			Com_Printf( "Base files:\n" );
		if( s->pack )
			Com_Printf( "%s (%s%i files)\n", s->pack->filename, ( s->pack->pure ? "pure, " : "" ), s->pack->numFiles );
		else
			Com_Printf( "%s\n", s->path );
	}
}

/*
   ============
   FS_CreateAbsolutePath

   Creates any directories needed to store the given filename
   ============
 */
void FS_CreateAbsolutePath( const char *path )
{
	char *ofs;

	for( ofs = ( char * )path + 1; *ofs; ofs++ )
	{
		if( *ofs == '/' )
		{
			// create the directory
			*ofs = 0;
			Sys_FS_CreateDirectory( path );
			*ofs = '/';
		}
	}
}

/*
   ================
   FS_AbsoluteNameForFile

   Gives absolute name for a game file
   NULL if not found, or file is in pak
   ================
 */
const char *FS_AbsoluteNameForFile( const char *filename )
{
	static char absolutename[1024]; // fixme
	searchpath_t *search = FS_SearchPathForFile( filename );

	if( !search || search->pack )
		return NULL;

	Q_snprintfz( absolutename, sizeof( absolutename ), "%s/%s", search->path, filename );
	return absolutename;
}

/*
   ================
   FS_AbsoluteNameForBaseFile

   Gives absolute name for a base file
   NULL if not found
   ================
 */
const char *FS_AbsoluteNameForBaseFile( const char *filename )
{
	static char absolutename[1024]; // fixme
	searchpath_t *search = FS_SearchPathForBaseFile( filename );

	if( !search )
		return NULL;

	Q_snprintfz( absolutename, sizeof( absolutename ), "%s/%s", search->path, filename );
	return absolutename;
}

/*
   ===========
   FS_BaseNameForFile
   ===========
 */
const char *FS_BaseNameForFile( const char *filename )
{
	const char *p;
	searchpath_t *search = FS_SearchPathForFile( filename );

	if( !search || search->pack )
		return NULL;

	// only give the basename part
	p = strrchr( search->path, '/' );

	if( !p )
		return va( "%s/%s", search->path, filename );
	else
		return va( "%s/%s", p+1, filename );
}

/*
   ================
   FS_GetGameDirectoryList
   ================
 */
int FS_GetGameDirectoryList( char *buf, size_t bufsize )
{
	char **modnames;
	int i, j, length, nummods, nummods_total;
	size_t len, alllen;
	const char *basename, *s;
	searchpath_t *basepath;

	if( !buf )
		return 0;

	nummods_total = 0;
	alllen = 0;
	buf[0] = '\0';

	basepath = fs_basepaths;
	while( basepath )
	{
		if( ( modnames = FS_ListFiles( va( "%s/*", basepath->path ), &nummods, SFF_SUBDIR, SFF_HIDDEN | SFF_SYSTEM ) ) )
		{
			for( i = 0; i < nummods - 1; i++ )
			{
				basename = COM_FileBase( modnames[i] );

				// forbidden directory?
				for( j = 0; forbidden_gamedirs[j]; j++ )
				{
					if( !Q_stricmp( forbidden_gamedirs[j], basename ) )
						break;
				}
				if( forbidden_gamedirs[j] )
					continue;

				// already added?
				s = buf;
				for( j = 0; j < nummods_total; j++, s += length + 1 )
				{
					length = strlen( s );
					if( !Q_stricmp( s, basename ) )
						break;
				}
				if( j != nummods_total )
					continue;

				// too little space?
				len = strlen( basename );
				if( bufsize <= len + alllen )
					break;

				// ok, add it
				strcpy( buf + alllen, basename );
				alllen += len + 1;
				buf[alllen] = '\0';
				Mem_ZoneFree( modnames[i] );
				nummods_total++;
			}
			Mem_ZoneFree( modnames );
		}
		basepath = basepath->next;
	}

	return nummods_total;
}

/*
   ================
   FS_AddGamePath
   ================
 */
static void FS_AddGamePath( const char *basepath, const char *gamedir )
{
	int i, numpaks, path_size;
	searchpath_t *search;
	pack_t *pak;
	char **paknames;

	FS_CheckTempnameSize( sizeof( char ) * ( strlen( basepath ) + 1 + strlen( gamedir ) + strlen( "/*.pk3" ) + 1 ) );
	Q_snprintfz( tempname, tempname_size, "%s/%s/*.pk3", basepath, gamedir );

	if( ( paknames = FS_ListFiles( tempname, &numpaks, 0, SFF_SUBDIR | SFF_HIDDEN | SFF_SYSTEM ) ) != 0 )
	{
		qsort( paknames, numpaks - 1, sizeof( char * ), ( int ( * )( const void *, const void * ) )FS_SortStrings );
		for( i = 0; i < numpaks - 1; i++ )
		{
			pak = FS_LoadPK3File( paknames[i], qfalse );
			if( !pak )
				continue;

			search = FS_Malloc( sizeof( searchpath_t ) );

			path_size = sizeof( char ) * ( COM_FilePathLength( paknames[i] ) + 1 );
			search->path = FS_Malloc( path_size );
			Q_strncpyz( search->path, paknames[i], path_size );

			search->pack = pak;
			search->next = fs_searchpaths;
			fs_searchpaths = search;
			Mem_ZoneFree( paknames[i] );
		}
		Mem_ZoneFree( paknames );
	}

	// add the directory to the search path
	search = FS_Malloc( sizeof( searchpath_t ) );

	path_size = sizeof( char ) * ( strlen( basepath ) + 1 + strlen( gamedir ) + 1 );
	search->path = FS_Malloc( path_size );
	Q_snprintfz( search->path, path_size, "%s/%s", basepath, gamedir );

	search->next = fs_searchpaths;
	fs_searchpaths = search;
}

/*
   ================
   FS_AddGameDirectory
   ================
 */
static void FS_AddGameDirectory( const char *gamedir )
{
	searchpath_t *compare, *search, *old, *prev, *basepath;

	old = fs_searchpaths;

	// add for every basepath, in reverse order
	prev = NULL;
	while( prev != fs_basepaths )
	{
		basepath = fs_basepaths;
		while( basepath->next != prev )
			basepath = basepath->next;
		FS_AddGamePath( basepath->path, gamedir );
		prev = basepath;
	}

	// scan for many paks with same name, but different base directory, and remove extra ones
	compare = fs_searchpaths;
	while( compare && compare != old )
	{
		if( compare->pack )
		{
			prev = compare;
			search = compare->next;
			while( search && search != old )
			{
				if( search->pack &&
				   !strcmp( COM_FileBase( search->pack->filename ), COM_FileBase( compare->pack->filename ) ) )
				{
					Com_Printf( "Removed duplicate pk3 file %s\n", search->pack->filename );
					prev->next = search->next;
					if( search->pack->sysHandle )
						Sys_FS_UnlockFile( search->pack->sysHandle );
					FS_Free( search->pack );
					FS_Free( search );
					search = prev;
				}

				prev = search;
				search = search->next;
			}
		}
		compare = compare->next;
	}
}

/*
   ================
   FS_SetGameDirectory

   Sets the gamedir and path to a different directory.
   ================
 */
qboolean FS_SetGameDirectory( const char *dir, qboolean force )
{
	int i;
	searchpath_t *next;

	if( !force && Com_ClientState() >= CA_CONNECTED && !Com_DemoPlaying() )
	{
		Com_Printf( "Can't change game directory while connected\n" );
		return qfalse;
	}

	Com_Printf( "Changing game directory to: %s\n", dir );

	if( !COM_ValidateRelativeFilename( dir ) )
	{
		Com_Printf( "Invalid name.\n" );
		return qfalse;
	}

	if( strchr( dir, '/' ) )
	{
		Com_Printf( "Game directory must be a single filename, not a path\n" );
		return qfalse;
	}

	for( i = 0; forbidden_gamedirs[i]; i++ )
	{
		if( !Q_stricmp( forbidden_gamedirs[i], dir ) )
		{
			Com_Printf( "Forbidden game directory\n" );
			return qfalse;
		}
	}

	// free up any current game dir info
	while( fs_searchpaths != fs_base_searchpaths )
	{
		if( fs_searchpaths->pack )
		{
			if( fs_searchpaths->pack->sysHandle )
				Sys_FS_UnlockFile( fs_searchpaths->pack->sysHandle );
			FS_Free( fs_searchpaths->pack->filename );
			FS_Free( fs_searchpaths->pack );
		}
		FS_Free( fs_searchpaths->path );
		next = fs_searchpaths->next;
		FS_Free( fs_searchpaths );
		fs_searchpaths = next;
	}

	if( !strcmp( dir, fs_basegame->string ) || ( *dir == 0 ) )
	{
		Cvar_ForceSet( "fs_game", fs_basegame->string );
	}
	else
	{
		Cvar_ForceSet( "fs_game", dir );
		FS_AddGameDirectory( dir );
	}

	// flush all data, so it will be forced to reload
	if( dedicated && !dedicated->integer )
	{
		Cbuf_AddText( "s_restart 1\nin_restart\n" );
		Cbuf_AddText( "exec autoexec.cfg\n" );
	}
	else
	{
		Cbuf_AddText( "exec dedicated_autoexec.cfg\n" );
	}
	Cbuf_Execute();

	return qtrue;
}

/*
   ================
   FS_AddBasePath
   ================
 */
static void FS_AddBasePath( const char *path )
{
	searchpath_t *new;

	new = FS_Malloc( sizeof( searchpath_t ) );
	new->path = FS_CopyString( path );
	new->next = fs_basepaths;
	fs_basepaths = new;
	COM_SanitizeFilePath( new->path );
}

/*
   ================
   FS_FreeSearchFiles
   ================
 */
static void FS_FreeSearchFiles( void )
{
	int i;

	// free temp memory
	for( i = 0; i < fs_cursearchfiles; i++ )
	{
		if( !fs_searchfiles[i].searchPath->pack )
			Mem_ZoneFree( fs_searchfiles[i].name );
	}
	fs_cursearchfiles = 0;
}

/*
   ================
   FS_Init
   ================
 */
void FS_Init( void )
{
	int i;

	assert( !fs_initialized );

	fs_mempool = Mem_AllocPool( NULL, "Filesystem" );

	Cmd_AddCommand( "fs_path", FS_Path_f );
	Cmd_AddCommand( "fs_pakfile", Cmd_PakFile_f ); // wsw : jal

	fs_numsearchfiles = FS_MIN_SEARCHFILES;
	fs_searchfiles = FS_Malloc( sizeof( searchfile_t ) * fs_numsearchfiles );

	fs_globalZipEntryUser = NULL;
	memset( fs_filehandles, 0, sizeof( fs_filehandles ) );

	//
	// link filehandles
	//
	fs_free_filehandles = fs_filehandles;
	fs_filehandles_headnode.prev = &fs_filehandles_headnode;
	fs_filehandles_headnode.next = &fs_filehandles_headnode;
	for( i = 0; i < FS_MAX_HANDLES - 1; i++ )
		fs_filehandles[i].next = &fs_filehandles[i+1];

	//
	// set basepaths
	//
	fs_cdpath = Cvar_Get( "fs_cdpath", "", CVAR_NOSET );
	fs_basepath = Cvar_Get( "fs_basepath", ".", CVAR_NOSET );
	if( Sys_FS_GetHomeDirectory() != NULL )
		fs_usehomedir = Cvar_Get( "fs_usehomedir", "1", CVAR_NOSET );

	if( fs_cdpath->string[0] )
		FS_AddBasePath( fs_cdpath->string );
	FS_AddBasePath( fs_basepath->string );
	if( Sys_FS_GetHomeDirectory() != NULL && fs_usehomedir->integer )
#ifdef __MACOSX__
		FS_AddBasePath( va( "%s/Library/Application Support/" APPLICATION, Sys_FS_GetHomeDirectory() ) );
#elif defined(_WIN32)
		FS_AddBasePath( va( "%s/%s", Sys_FS_GetHomeDirectory(), APPLICATION ) );
#else	// Unix
		FS_AddBasePath( va( "%s/.%c%s", Sys_FS_GetHomeDirectory(), tolower( *( (const char *)APPLICATION ) ),
		                    ( (const char *)APPLICATION ) + 1 ) );
#endif

	//
	// set game directories
	//
	fs_basegame = Cvar_Get( "fs_basegame", DEFAULT_BASEGAME, CVAR_NOSET );
	if( !fs_basegame->string[0] )
		Cvar_ForceSet( "fs_basegame", DEFAULT_BASEGAME );
	fs_game = Cvar_Get( "fs_game", fs_basegame->string, CVAR_LATCH|CVAR_SERVERINFO );
	if( !fs_game->string[0] )
		Cvar_ForceSet( "fs_game", fs_basegame->string );

	FS_AddGameDirectory( fs_basegame->string );

	fs_base_searchpaths = fs_searchpaths;

	if( strcmp( fs_game->string, fs_basegame->string ) )
		FS_SetGameDirectory( fs_game->string, qfalse );

	// done
	Com_Printf( "Using %s for writing\n", FS_WriteDirectory() );

	fs_cursearchfiles = 0;

	fs_initialized = qtrue;
}

/*
   ================
   FS_Frame
   ================
 */
void FS_Frame( void )
{
	FS_FreeSearchFiles();
}

/*
   ================
   FS_Shutdown
   ================
 */
void FS_Shutdown( void )
{
	searchpath_t *search;

	if( !fs_initialized )
		return;

	Cmd_RemoveCommand( "fs_path" );
	Cmd_RemoveCommand( "fs_pakfile" );

	FS_FreeSearchFiles();
	FS_Free( fs_searchfiles );
	fs_numsearchfiles = 0;

	while( fs_searchpaths )
	{
		search = fs_searchpaths;
		fs_searchpaths = search->next;

		if( search->pack )
		{
			if( search->pack->sysHandle )
				Sys_FS_UnlockFile( search->pack->sysHandle );
			FS_Free( search->pack->filename );
			FS_Free( search->pack );
		}
		FS_Free( search->path );
		FS_Free( search );
	}

	while( fs_basepaths )
	{
		search = fs_basepaths;
		fs_basepaths = search->next;

		FS_Free( search->path );
		FS_Free( search );
	}

	if( tempname )
	{
		FS_Free( tempname );
		tempname = NULL;
		tempname_size = 0;
	}

	Mem_FreePool( &fs_mempool );

	fs_initialized = qfalse;
}
