/*
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 <zlib.h>
#include "unzip.h"

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

QUAKE FILESYSTEM

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

#define MAX_FILES_IN_PK2	0x4000
#define MAX_FILE_HANDLES	8

#define FS_Malloc( size )		Z_TagMalloc( size, TAG_FILESYSTEM )
#define FS_CopyString( string )		Z_TagCopyString( string, TAG_FILESYSTEM )

#ifdef _WIN32
#define FS_strcmp	Q_stricmp
#else
#define FS_strcmp	strcmp
#endif

//
// in memory
//

typedef struct packfile_s {
	char	*name;
	int		filepos;
	int		filelen;

	struct packfile_s *hashNext;
} packfile_t;

typedef struct pack_s {
	unzFile zFile;
	FILE	*fp;
	int		numfiles;
	packfile_t	*files;
	packfile_t	**fileHash;
	int			hashSize;
	char	filename[1];
} pack_t;

typedef enum fsFileType_e {
	FS_FREE,
	FS_REAL,
	FS_PAK,
	FS_PK2,
	FS_GZIP,
	FS_BAD
} fsFileType_t;

typedef struct fsFile_s {
	char	name[MAX_QPATH];
	char	fullpath[MAX_OSPATH];
	fsFileType_t type;
	uint32	mode;
	FILE *fp;
	void *zfp;
	packfile_t	*pak;
	qboolean unique;
	int	length;
} fsFile_t;

typedef struct searchpath_s {
	pack_t	*pack;		// only one of filename / pack will be used
	struct searchpath_s *next;
	char	filename[1];
} searchpath_t;

typedef struct fsLink_s {
    char *target;
    int targetLength, nameLength;
    struct fsLink_s *next;
    char name[1];
} fsLink_t;

static char		fs_gamedir[MAX_OSPATH];

static cvar_t	*fs_basedir;
static cvar_t	*fs_cddir;
static cvar_t	*fs_game;
static cvar_t	*fs_debug;
static cvar_t	*fs_restrict_mask;

static searchpath_t	*fs_searchpaths;
static searchpath_t	*fs_base_searchpaths;

static fsLink_t     *fs_links;

static fsFile_t		fs_files[MAX_FILE_HANDLES];

static qboolean		fs_fileFromPak;

fsAPI_t		fs;

void FS_AddGameDirectory( const char *fmt, ... )
    __attribute__(( format( printf, 1, 2 ) ));

/*

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.

The "base directory" is the path to the directory holding the quake.exe and all game directories.
The sys_* files pass this to host_init in quakeparms_t->basedir. 
This can be overridden with the "-basedir" command line parm to allow code debugging in a different directory. 
The base directory is only used during filesystem initialization.

The "game directory" is the first tree on the search path and directory that
all generated files (savegames, screenshots, demos, config files) will be saved to.
This can be overridden with the "-game" command line parameter.
The game directory can never be changed while quake is executing.
This is a precacution against having a malicious server instruct clients to write files over areas they shouldn't.

*/

/*
================
FS_DPrintf
================
*/
static void FS_DPrintf( char *format, ... ) {
	va_list argptr;
	char string[MAXPRINTMSG];

	if( !fs_debug || !fs_debug->integer ) {
		return;
	}

	va_start( argptr, format );
	Q_vsnprintf( string, sizeof( string ), format, argptr );
	va_end( argptr );

	Com_Printf( S_COLOR_CYAN "%s", string );
}

/*
================
FS_AllocHandle
================
*/
static fsFile_t *FS_AllocHandle( fileHandle_t *f, const char *name ) {
	fsFile_t *file;
	int i;

	for( i = 0, file = fs_files; i < MAX_FILE_HANDLES; i++, file++ ) {
		if( file->type == FS_FREE ) {
			break;
		}
	}

	if( i == MAX_FILE_HANDLES ) {
		Com_Error( ERR_FATAL, "FS_AllocHandle: none free" );
	}

	Q_strncpyz( file->name, name, sizeof( file->name ) );
	Q_strlwr( file->name );
	*f = i + 1;
	return file;
}

/*
================
FS_FileForHandle
================
*/
static fsFile_t *FS_FileForHandle( fileHandle_t f ) {
	fsFile_t *file;

	if( f <= 0 || f >= MAX_FILE_HANDLES + 1 ) {
		Com_Error( ERR_FATAL, "FS_FileForHandle: invalid handle: %i", f );
	}

	file = &fs_files[f - 1];
	if( file->type == FS_FREE ) {
		Com_Error( ERR_FATAL, "FS_FileForHandle: free file: %i", f );
	}

	if( file->type < FS_FREE || file->type >= FS_BAD ) {
		Com_Error( ERR_FATAL, "FS_FileForHandle: invalid file type: %i", file->type );
	}

	return file;
}

qboolean FS_ValidatePath( const char *s ) {
	const char *start;
	int back;

	// check for leading slash
	// check for empty path
	if( *s == '/' || *s == '\\' || *s == 0 ) {
		return qfalse;
	}

	back = 0;
	start = s;
	while( *s ) {
		// check for ".."
		if( *s == '.' && s[1] == '.' ) {
			if( back > 1 ) {
				return qfalse;
			}
			back++;
		}
		if( *s == '/' || *s == '\\' ) {
			// check for two slashes in a row
			// check for trailing slash
			if( ( s[1] == '/' || s[1] == '\\' || s[1] == 0 ) ) {
				return qfalse;
			}
		}
		if( *s == ':' ) {
			// check for "X:\"
			if( s[1] == '\\' || s[1] == '/' ) {
				return qfalse;
			}
		}
		s++;
	}

	if( s - start > MAX_QPATH ) {
		return qfalse;
	}

	return qtrue;
}

/*
================
FS_ConvertToSysPath
================
*/
static char *FS_ConvertToSysPath( char *path ) {
	char *s;

	s = path;
	while( *s ) {
		if( *s == '/' || *s == '\\' ) {
			*s = PATH_SEP_CHAR;
		}
		s++;
	}

	return path;

}

/*
================
FS_GetFileLength

Gets current file length.
For GZIP files, returns uncompressed length
(very slow operation because it has to uncompress the whole file).
================
*/
int FS_GetFileLength( fileHandle_t f ) {
	fsFile_t *file = FS_FileForHandle( f );
	int pos, length;

	if( ( file->mode & FS_MODE_MASK ) == FS_MODE_READ ) {
		if( file->type == FS_GZIP ) {
			goto gzipHack;
		}
		return file->length;
	}

	switch( file->type ) {
	case FS_REAL:
		pos = ftell( file->fp );
		fseek( file->fp, 0, SEEK_END );
		length = ftell( file->fp );
		fseek( file->fp, pos, SEEK_SET );
		break;
	case FS_GZIP:
gzipHack:
		pos = gztell( file->zfp );
		gzseek( file->zfp, 0x7FFFFFFF, SEEK_SET );
		length = gztell( file->zfp );
		gzseek( file->zfp, pos, SEEK_SET );
		break;
	default:
		Com_Error( ERR_FATAL, "FS_GetFileLength: bad file type" );
		length = -1;
		break;
	}

	return length;
}

int FS_GetFileLengthNoCache( fileHandle_t f ) {
	fsFile_t *file = FS_FileForHandle( f );
	int pos, length;

	switch( file->type ) {
	case FS_REAL:
		pos = ftell( file->fp );
		fseek( file->fp, 0, SEEK_END );
		length = ftell( file->fp );
		fseek( file->fp, pos, SEEK_SET );
		break;
	case FS_GZIP:
		pos = gztell( file->zfp );
		gzseek( file->zfp, 0x7FFFFFFF, SEEK_SET );
		length = gztell( file->zfp );
		gzseek( file->zfp, pos, SEEK_SET );
		break;
	case FS_PAK:
	case FS_PK2:
		length = file->length;
		break;
	default:
		Com_Error( ERR_FATAL, "FS_GetFileLengthNoCache: bad file type" );
		length = -1;
		break;
	}

	return length;
}

/*
================
FS_GetFileName
================
*/
const char *FS_GetFileName( fileHandle_t f ) {
	return ( FS_FileForHandle( f ) )->name;
}

const char *FS_GetFileFullPath( fileHandle_t f ) {
	return ( FS_FileForHandle( f ) )->fullpath;
}

/*
============
FS_CreatePath

Creates any directories needed to store the given filename.
Expects a fully qualified path.
============
*/
void FS_CreatePath( const char *path ) {
	char buffer[MAX_OSPATH];
	char *ofs;

	Q_strncpyz( buffer, path, sizeof( buffer ) );
	FS_ConvertToSysPath( buffer );

	FS_DPrintf( "FS_CreatePath( '%s' )\n", buffer );
	
	for( ofs = buffer + 1; *ofs; ofs++ ) {
		if( *ofs == PATH_SEP_CHAR ) {	
			// create the directory
			*ofs = 0;
			Sys_Mkdir( buffer );
			*ofs = PATH_SEP_CHAR;
		}
	}
}


/*
==============
FS_FCloseFile
==============
*/
void FS_FCloseFile( fileHandle_t f ) {
	fsFile_t *file = FS_FileForHandle( f );

	FS_DPrintf( "FS_FCloseFile( '%s' )\n", file->fullpath );

	switch( file->type ) {
	case FS_REAL:
		fclose( file->fp );
		break;
	case FS_PAK:
		if( file->unique ) {
			fclose( file->fp );
		}
		break;
	case FS_GZIP:
		gzclose( file->zfp );
		break;
	case FS_PK2:
		unzCloseCurrentFile( file->zfp );
		if( file->unique ) {
			unzClose( file->zfp );
		}
		break;
	default:
		break;
	}

	/* don't clear name and mode, so post-restart reopening works */
	file->type = FS_FREE;
	file->fp = NULL;
	file->zfp = NULL;
	file->pak = NULL;
	file->unique = qfalse;
}

/*
============
FS_FOpenFileWrite

In case of GZIP files, returns *raw* (compressed) length!
============
*/
static int FS_FOpenFileWrite( fsFile_t *file ) {
	FILE *fp;
	gzFile zfp;
	char *modeStr;
	fsFileType_t type;
	uint32 mode;
	char *ext;

#ifdef _WIN32
	/* allow writing into basedir on Windows */
	if( ( file->mode & FS_PATH_MASK ) == FS_PATH_BASE ) {
		Com_sprintf( file->fullpath, sizeof( file->fullpath ),
			"%s/" BASEDIRNAME "/%s", fs_basedir->string, file->name );
	} else
#endif
	{
		Com_sprintf( file->fullpath, sizeof( file->fullpath ),
			"%s/%s", fs_gamedir, file->name );
	}

	mode = file->mode & FS_MODE_MASK;
	switch( mode ) {
	case FS_MODE_APPEND:
		modeStr = "ab";
		break;
	case FS_MODE_WRITE:
		modeStr = "wb";
		break;
	case FS_MODE_RDWR:
		modeStr = "r+b";
		break;
	default:
		Com_Error( ERR_FATAL, "FS_FOpenFileWrite( '%s' ): invalid mode mask",
			file->fullpath );
		modeStr = NULL;
		break;
	}

	FS_ConvertToSysPath( file->fullpath );

	FS_CreatePath( file->fullpath );

	fp = fopen( file->fullpath, modeStr );
	
	if( !fp ) {
		FS_DPrintf( "FS_FOpenFileWrite: fopen( '%s', '%s' ) failed\n",
			file->fullpath, modeStr );
		return -1;
	}

	type = FS_REAL;
	if( !( file->mode & FS_FLAG_RAW ) ) {
		ext = COM_FileExtension( file->fullpath );
		if( !strcmp( ext, ".gz" ) ) {
			zfp = gzdopen( fileno( fp ), modeStr );
			if( !zfp ) {
				FS_DPrintf( "FS_FOpenFileWrite: gzopen( '%s', '%s' ) failed\n",
					file->fullpath, modeStr );
				fclose( fp );
				return -1;
			}
			file->zfp = zfp;
			type = FS_GZIP;
		}
	}

	FS_DPrintf( "FS_FOpenFileWrite( '%s' )\n", file->fullpath );

	file->fp = fp;
	file->type = type;
	file->length = 0;
	file->unique = qtrue;

	if( mode == FS_MODE_WRITE ) {
		return 0;
	}

	if( mode == FS_MODE_RDWR ) {
		fseek( fp, 0, SEEK_END );
	}
	
	return ftell( fp );
}


/*
===========
FS_FOpenFileRead

Finds the file in the search path.
Fills fsFile_t and returns file length.
In case of GZIP files, returns *raw* (compressed) length!
Used for streaming data out of either a pak file or
a seperate file.
===========
*/
static int FS_FOpenFileRead( fsFile_t *file, qboolean unique ) {
	searchpath_t	*search;
	pack_t			*pak;
	uint32			hash;
	packfile_t		*entry;
	FILE			*fp;
	void			*zfp;
	fsFileType_t	type;
	int				pos, length;
	char			*ext;

	fs_fileFromPak = qfalse;

//
// search through the path, one element at a time
//
	if( ( file->mode & FS_PATH_MASK ) == FS_PATH_BASE ) {
		search = fs_base_searchpaths;
	} else {
		search = fs_searchpaths;
	}

	hash = Com_HashPath( file->name, 0 );

	for( ; search; search = search->next ) {
		if( ( file->mode & FS_PATH_MASK ) == FS_PATH_GAME ) {
			if( fs_searchpaths != fs_base_searchpaths && search == fs_base_searchpaths ) {
				/* consider baseq2 a gamedir if no gamedir loaded */
				break;
			}
		}
	
	// is the element a pak file?
		if( search->pack ) {
			if( ( file->mode & FS_TYPE_MASK ) == FS_TYPE_REAL ) {
				continue;
			}
		// look through all the pak file elements
			pak = search->pack;
			entry = pak->fileHash[ hash & ( pak->hashSize - 1 ) ];
			for( ; entry; entry = entry->hashNext ) {
				// both entry->name and file->name should be already lowercased
				if( !strcmp( entry->name, file->name ) ) {	
					// found it!
					fs_fileFromPak = qtrue;

					Com_sprintf( file->fullpath, sizeof( file->fullpath ), "%s/%s", pak->filename, file->name );

					// open a new file on the pakfile
					if( pak->zFile ) {
						if( unique ) {
							zfp = unzReOpen( pak->filename, pak->zFile );
							if( !zfp ) {
								Com_Error( ERR_FATAL, "FS_FOpenFileRead: unzReOpen( '%s' ) failed", pak->filename );
							}
						} else {
							zfp = pak->zFile;
						}
						if( unzSetCurrentFileInfoPosition( zfp, entry->filepos ) == -1 ) {
							Com_Error( ERR_FATAL, "FS_FOpenFileRead: unzSetCurrentFileInfoPosition( '%s/%s' ) failed", pak->filename, entry->name );
						}
						if( unzOpenCurrentFile( zfp ) != UNZ_OK ) {
							Com_Error( ERR_FATAL, "FS_FOpenFileRead: unzReOpen( '%s/%s' ) failed", pak->filename, entry->name );
						}

						file->zfp = zfp;
						file->type = FS_PK2;
					} else {
						if( unique ) {
							fp = fopen( pak->filename, "rb" );
							if( !fp ) {
								Com_Error( ERR_FATAL, "Couldn't reopen %s", pak->filename );
							}
						} else {
							fp = pak->fp;
						}

						fseek( fp, entry->filepos, SEEK_SET );

						file->fp = fp;
						file->type = FS_PAK;
					}

					length = entry->filelen;
					file->pak = entry;
					file->length = length;
					file->unique = unique;

					FS_DPrintf( "FS_FOpenFileRead( '%s/%s' )\n", pak->filename, file->name );

					return length;
				}
			}
		} else {
			if( ( file->mode & FS_TYPE_MASK ) == FS_TYPE_PAK ) {
				continue;
			}
	// check a file in the directory tree
			Com_sprintf( file->fullpath, sizeof( file->fullpath ), "%s/%s",
				search->filename, file->name );

			FS_ConvertToSysPath( file->fullpath );

			fp = fopen( file->fullpath, "rb" );
			if( !fp ) {
				continue;
			}

			type = FS_REAL;
			if( !( file->mode & FS_FLAG_RAW ) ) {
				ext = COM_FileExtension( file->fullpath );
				if( !strcmp( ext, ".gz" ) ) {
					zfp = gzdopen( fileno( fp ), "rb" );
					if( !zfp ) {
						Com_WPrintf( "gzopen( '%s', 'rb' ) failed, "
							"not a GZIP file?\n", file->fullpath );
						fclose( fp );
						return -1;
					}
					file->zfp = zfp;
					type = FS_GZIP;
				}
			}
			
			FS_DPrintf( "FS_FOpenFileRead( '%s' )\n", file->fullpath );

			file->fp = fp;
			file->type = type;
			file->unique = qtrue;

			pos = ftell( fp );
			fseek( fp, 0, SEEK_END );
			length = ftell( fp );
			fseek( fp, pos, SEEK_SET );

			file->length = length;

			return length;
		}
	}
	
	FS_DPrintf( "FS_FOpenFileRead( '%s' ): couldn't find\n", file->name );
	
	return -1;
}

/*
=================
FS_LastFileFromPak
=================
*/
qboolean FS_LastFileFromPak( void ) {
	return fs_fileFromPak;
}


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

Properly handles partial reads
=================
*/
void CDAudio_Stop( void );

#define	MAX_READ	0x40000		// read in blocks of 256k

int FS_Read( void *buffer, int len, fileHandle_t hFile ) {
	int		block, remaining;
	int		read;
	byte	*buf;
	int		tries;
	fsFile_t	*file = FS_FileForHandle( hFile );

	buf = (byte *)buffer;

	// read in chunks for progress bar
	remaining = len;
	tries = 0;
	read = 0;
	while( remaining ) {
		block = remaining;
		if( block > MAX_READ )
			block = MAX_READ;
		switch( file->type ) {
		case FS_REAL:
		case FS_PAK:
			read = fread( buf, 1, block, file->fp );
			break;
		case FS_GZIP:
			read = gzread( file->zfp, buf, block );
			break;
		case FS_PK2:
			read = unzReadCurrentFile( file->zfp, buf, block );
			break;
		default:
			break;
		}
		if( read == 0 ) {
#ifndef DEDICATED_ONLY
			// we might have been trying to read from a CD
			if( !tries ) {
				tries = 1;
				CDAudio_Stop();
			} else
#endif // DEDICATED_ONLY
			{
				return len - remaining;
			}
		}

		if( read == -1 )
			Com_Error( ERR_FATAL, "FS_Read: -1 bytes read" );

		// do some progress bar thing heref...

		remaining -= read;
		buf += read;
	}

	return len;
}

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

Properly handles partial writes
=================
*/
#define	MAX_WRITE	0x40000		// write in blocks of 256k
int FS_Write( const void *buffer, int len, fileHandle_t hFile ) {
	int		block, remaining;
	int		write;
	byte	*buf;
	fsFile_t	*file = FS_FileForHandle( hFile );

	buf = (byte *)buffer;

	// read in chunks for progress bar
	remaining = len;
	write = 0;
	while( remaining ) {
		block = remaining;
		if( block > MAX_WRITE )
			block = MAX_WRITE;
		switch( file->type ) {
		case FS_REAL:
			write = fwrite( buf, 1, block, file->fp );
			break;
		case FS_GZIP:
			write = gzwrite( file->zfp, buf, block );
			break;
		default:
			Com_Error( ERR_FATAL, "FS_Write: illegal file type" );
			break;
		}
		if( write == 0 ) {
			return len - remaining;
		}

		if( write == -1 ) {
			Com_Error( ERR_FATAL, "FS_Write: -1 bytes written" );
		}

		// do some progress bar thing here...

		remaining -= write;
		buf += write;
	}


	if( ( file->mode & FS_FLUSH_MASK ) == FS_FLUSH_SYNC ) {
		switch( file->type ) {
		case FS_REAL:
			fflush( file->fp );
			break;
		case FS_GZIP:
			gzflush( file->zfp, Z_FINISH );
			break;
		default:
			break;
		}
	}

	return len;
}


static char *FS_ExpandLinks( const char *filename ) {
    static char        buffer[MAX_QPATH];
    fsLink_t    *l;
    int         length;

    length = strlen( filename );
    for( l = fs_links; l; l = l->next ) {
        if( l->nameLength > length ) {
            continue;
        }
        if( !Q_stricmpn( l->name, filename, l->nameLength ) ) {
            if( l->targetLength + length - l->nameLength > MAX_QPATH - 1 ) {
                FS_DPrintf( "FS_ExpandLinks( '%s' ): MAX_QPATH exceeded\n",
                        filename );
                return ( char * )filename;
            }
            memcpy( buffer, l->target, l->targetLength );
            strcpy( buffer + l->targetLength, filename + l->nameLength );
            FS_DPrintf( "FS_ExpandLinks( '%s' ) --> '%s'\n", filename, buffer );
            return buffer;
        }
    }

    return ( char * )filename;
}

/*
============
FS_FOpenFile
============
*/
int FS_FOpenFile( const char *filename, fileHandle_t *f, uint32 mode ) {
	fsFile_t	*file;
	fileHandle_t hFile;
	int			ret = -1;

	if( !filename || !f ) {
		Com_Error( ERR_FATAL, "FS_FOpenFile: NULL" );
	}

	*f = 0;

	if( !fs_searchpaths ) {
		return -1; /* not yet initialized */
	}

    if( ( mode & FS_MODE_MASK ) == FS_MODE_READ ) {
        filename = FS_ExpandLinks( filename );
    }

	if( !FS_ValidatePath( filename ) ) {
		FS_DPrintf( "FS_FOpenFile: refusing invalid path: %s\n", filename );
		return -1;
	}

	/* allocate new file handle */
	file = FS_AllocHandle( &hFile, filename );
	file->mode = mode;

	mode &= FS_MODE_MASK;
	switch( mode ) {
	case FS_MODE_READ:
		ret = FS_FOpenFileRead( file, qtrue );
		break;
	case FS_MODE_WRITE:
	case FS_MODE_APPEND:
	case FS_MODE_RDWR:
		ret = FS_FOpenFileWrite( file );
		break;
	default:
		Com_Error( ERR_FATAL, "FS_FOpenFile: illegal mode: %u", mode );
		break;
	}

	/* if succeeded, store file handle */
	if( ret != -1 ) {
		*f = hFile;
	}

	return ret;
}


/*
============
FS_Tell
============
*/
int FS_Tell( fileHandle_t f ) {
	fsFile_t *file = FS_FileForHandle( f );
	int length;

	switch( file->type ) {
	case FS_REAL:
		length = ftell( file->fp );
		break;
	case FS_GZIP:
		length = gztell( file->zfp );
		break;
	case FS_PAK:
		length = ftell( file->fp ) - file->pak->filepos;
		break;
	case FS_PK2:
		length = unztell( file->zfp );
		break;
	default:
		length = -1;
		break;
	}

	return length;
}

/*
============
FS_RawTell
============
*/
int FS_RawTell( fileHandle_t f ) {
	fsFile_t *file = FS_FileForHandle( f );
	int length;

	switch( file->type ) {
	case FS_REAL:
	case FS_GZIP:
		length = ftell( file->fp );
		break;
	case FS_PAK:
		length = ftell( file->fp ) - file->pak->filepos;
		break;
	case FS_PK2:
		length = unztell( file->zfp );
		break;
	default:
		length = -1;
		break;
	}

	return length;
}

#define MAX_LOAD_BUFFER		0x80000		// 512k

/* static buffer for small, possibly nested file loads */
static byte loadBuffer[MAX_LOAD_BUFFER];
static int loadInuse;
static int loadStack;

static int loadCount;
static int loadCountStatic;

/* very simple one-file cache for *.bsp files */
static byte *cachedBytes;
static char cachedPath[MAX_QPATH];
static int	cachedLength;
static int  cachedInuse;


/*
============
FS_LoadFile

Filenames are relative to the quake search path
a null buffer will just return the file length without loading
============
*/
int FS_LoadFileEx( const char *path, void **buffer, uint32 flags ) {
	fsFile_t *file;
	fileHandle_t f;
	byte	*buf;
	int		length;

	if( !path ) {
		Com_Error( ERR_FATAL, "FS_LoadFile: NULL" );
	}

	if( buffer ) {
		*buffer = NULL;
	}

	if( !fs_searchpaths ) {
		return -1; /* not yet initialized */
	}

    path = FS_ExpandLinks( path );

	if( !FS_ValidatePath( path ) ) {
		FS_DPrintf( "FS_LoadFile: refusing invalid path: %s\n", path );
		return -1;
	}

	if( buffer && ( flags & FS_FLAG_CACHE ) ) {
		if( !strcmp( cachedPath, path ) ) {
			//Com_Printf( S_COLOR_MAGENTA"cached: %s\n", path );
			*buffer = cachedBytes;
			cachedInuse++;
			return cachedLength;
		}
	}

	/* allocate new file handle */
	file = FS_AllocHandle( &f, path );
	flags &= ~FS_MODE_MASK;
	file->mode = flags | FS_MODE_READ;

	/* look for it in the filesystem or pack files */
	length = FS_FOpenFileRead( file, qfalse );
	if( length == -1 ) {
		return -1;
	}

	/* get real file length */
	length = FS_GetFileLength( f );
	
	if( buffer ) {
		if( !( flags & FS_FLAG_CACHE ) && loadInuse + length < MAX_LOAD_BUFFER ) {
			buf = &loadBuffer[loadInuse];
			loadInuse += length + 1;
			loadInuse = ( loadInuse + 3 ) & ~3;
			loadStack++;
			loadCountStatic++;
		} else {
			//Com_Printf( S_COLOR_MAGENTA"alloc: %s\n", path );
			buf = FS_Malloc( length + 1 );
			loadCount++;
		}
		*buffer = buf;

		FS_Read( buf, length, f );
		buf[length] = 0;

		if( flags & FS_FLAG_CACHE ) {
			FS_FlushCache();
			cachedBytes = buf;
			cachedLength = length;
			strcpy( cachedPath, path );
			cachedInuse = 1;
		}
	}

	FS_FCloseFile( f );

	return length;
}

int FS_LoadFile( const char *path, void **buffer ) {
	return FS_LoadFileEx( path, buffer, 0 );
}


/*
=============
FS_FreeFile
=============
*/
void FS_FreeFile( void *buffer ) {
	if( !buffer ) {
		Com_Error( ERR_FATAL, "FS_FreeFile: NULL" );
	}
	if( ( byte * )buffer == cachedBytes ) {
		if( cachedInuse == 0 ) {
			Com_Error( ERR_FATAL, "FS_FreeFile: cachedInuse is zero" );
		}
		cachedInuse--;
		return;
	}
	if( ( byte * )buffer >= loadBuffer && ( byte * )buffer < loadBuffer + MAX_LOAD_BUFFER ) {
		if( loadStack == 0 ) {
			Com_Error( ERR_FATAL, "FS_FreeFile: loadStack is zero" );
		}
		loadStack--;
		if( loadStack == 0 ) {
			loadInuse = 0;
		}
	} else {
		Z_Free( buffer );
	}
}

void FS_FlushCache( void ) {
	if( cachedInuse ) {
		Com_Error( ERR_FATAL, "FS_FlushCache: cachedInuse is nonzero" );
	}
	if( !cachedBytes ) {
		return;
	}
	Z_Free( cachedBytes );
	cachedBytes = NULL;
	cachedLength = 0;
	cachedPath[0] = 0;
}

/*
=============
FS_CopyFile
=============
*/
qboolean FS_CopyFile( const char *src, const char *dst ) {
	fileHandle_t hSrc, hDst;
	byte	buffer[MAX_READ];
	int		len;
	int		size;

	FS_DPrintf( "FS_CopyFile( '%s', '%s' )\n", src, dst );

	size = FS_FOpenFile( src, &hSrc, FS_MODE_READ );
	if( !hSrc ) {
		return qfalse;
	}

	FS_FOpenFile( dst, &hDst, FS_MODE_WRITE );
	if( !hDst ) {
		FS_FCloseFile( hSrc );
		return qfalse;
	}

	while( size ) {
		len = size;
		if( len > sizeof( buffer ) ) {
			len = sizeof( buffer );
		}
		if( !( len = FS_Read( buffer, len, hSrc ) ) ) {
			break;
		}
		FS_Write( buffer, len, hDst );
		size -= len;
	}

	FS_FCloseFile( hSrc );
	FS_FCloseFile( hDst );

	if( size ) {
		return qfalse;
	}

	return qtrue;
}

/*
================
FS_RemoveFile
================
*/
qboolean FS_RemoveFile( const char *filename ) {
	char path[MAX_OSPATH];

	if( !FS_ValidatePath( filename ) ) {
		FS_DPrintf( "FS_RemoveFile: refusing invalid path: %s\n", filename );
		return qfalse;
	}

	Com_sprintf( path, sizeof( path ), "%s/%s", fs_gamedir, filename );
	FS_ConvertToSysPath( path );

	if( !Sys_RemoveFile( path ) ) {
		return qfalse;
	}

	return qtrue;
}

/*
================
FS_RemoveFile
================
*/
qboolean FS_RenameFile( const char *from, const char *to ) {
	char frompath[MAX_OSPATH];
	char topath[MAX_OSPATH];

	if( !FS_ValidatePath( from ) || !FS_ValidatePath( to ) ) {
		FS_DPrintf( "FS_RenameFile: %s, %s: refusing invalid path\n", from, to );
		return qfalse;
	}

	Com_sprintf( frompath, sizeof( frompath ), "%s/%s", fs_gamedir, from );
	Com_sprintf( topath, sizeof( topath ), "%s/%s", fs_gamedir, to );

	FS_ConvertToSysPath( frompath );
	FS_ConvertToSysPath( topath );

	if( !Sys_RenameFile( frompath, topath ) ) {
		FS_DPrintf( "FS_RenameFile: Sys_RenameFile( '%s', '%s' ) failed\n", frompath, topath );
		return qfalse;
	}

	return qtrue;
}

/*
================
FS_FPrintf
================
*/
void FS_FPrintf( fileHandle_t f, const char *format, ... ) {
	va_list argptr;
	char string[MAXPRINTMSG];
	int len;

	va_start( argptr, format );
	len = Q_vsnprintf( string, sizeof( string ), format, argptr );
	va_end( argptr );

	FS_Write( string, len, f );
}

/*
=================
FS_LoadPakFile

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_LoadPakFile( const char *packfile ) {
	dpackheader_t	header;
	int				i;
	packfile_t		*file;
	dpackfile_t		*dfile;
	int				numpackfiles;
	char			*names;
	int				namesLength;
	pack_t			*pack;
	FILE			*packhandle;
	dpackfile_t		info[MAX_FILES_IN_PACK];
	int				hashSize;
	uint32			hash;
	int				len;

	packhandle = fopen( packfile, "rb" );
	if( !packhandle ) {
		Com_WPrintf( "Couldn't open %s\n", packfile );
		return NULL;
	}

	fread( &header, 1, sizeof( header ), packhandle );

	if( LittleLong( header.ident ) != IDPAKHEADER ) {
		Com_WPrintf( "%s is not a 'PACK' file\n", packfile );
		goto fail;
	}
	header.dirofs = LittleLong( header.dirofs );
	header.dirlen = LittleLong( header.dirlen );

	if( header.dirlen % sizeof( dpackfile_t ) ) {
		Com_WPrintf( "%s has bad directory length\n", packfile );
		goto fail;
	}

	numpackfiles = header.dirlen / sizeof( dpackfile_t );

	if( numpackfiles > MAX_FILES_IN_PACK ) {
		Com_WPrintf( "%s has too many files, %i > %i\n", packfile, numpackfiles, MAX_FILES_IN_PACK );
		//if( numpackfiles > MAX_FILES_IN_PK2 ) {
			goto fail;
		//}
	}

	fseek( packhandle, header.dirofs, SEEK_SET );
	fread( info, 1, header.dirlen, packhandle );

	namesLength = 0;
	for( i = 0, dfile = info; i < numpackfiles; i++, dfile++ ) {
		dfile->name[sizeof( dfile->name ) - 1] = 0;
		namesLength += strlen( dfile->name ) + 1;
	}

	hashSize = Q_CeilPowerOfTwo( numpackfiles );
	if( hashSize > 32 ) {
		hashSize >>= 1;
	}

	len = strlen( packfile );
	pack = FS_Malloc( sizeof( pack_t ) +
		numpackfiles * sizeof( packfile_t ) +
		hashSize * sizeof( packfile_t * ) +
		namesLength + len );
	strcpy( pack->filename, packfile );
	pack->fp = packhandle;
	pack->zFile = NULL;
	pack->numfiles = numpackfiles;
	pack->hashSize = hashSize;
	pack->files = ( packfile_t * )( ( byte * )pack + sizeof( pack_t ) + len );
	pack->fileHash = ( packfile_t ** )( pack->files + numpackfiles );
	names = ( char * )( pack->fileHash + hashSize );
	memset( pack->fileHash, 0, hashSize * sizeof( packfile_t * ) );

// parse the directory
	for( i = 0, file = pack->files, dfile = info; i < pack->numfiles; i++, file++, dfile++ ) {
		len = strlen( dfile->name ) + 1;
		file->name = names;

		strcpy( file->name, dfile->name );
		Q_strlwr( file->name );

		file->filepos = LittleLong( dfile->filepos );
		file->filelen = LittleLong( dfile->filelen );

		hash = Com_HashPath( file->name, hashSize );
		file->hashNext = pack->fileHash[hash];
		pack->fileHash[hash] = file;

		names += len;
	}

	FS_DPrintf( "Added pakfile '%s', %d files, %d hash table entries\n",
		packfile, numpackfiles, hashSize );

	return pack;

fail:
	fclose( packhandle );
	return NULL;
}



/*
=================
FS_LoadZipFile
=================
*/
static pack_t *FS_LoadZipFile( const char *packfile ) {
	int				i;
	packfile_t		*file;
	char			*names;
	int				numFiles;
	pack_t			*pack;
	unzFile			zFile;
	unz_global_info	zGlobalInfo;
	unz_file_info	zInfo;
	char			name[MAX_QPATH];
	int				namesLength;
	int				hashSize;
	uint32			hash;
	int				len;

	zFile = unzOpen( packfile );
	if( !zFile ) {
		Com_WPrintf( "unzOpen() failed on %s\n", packfile );
		return NULL;
	}

	if( unzGetGlobalInfo( zFile, &zGlobalInfo ) != UNZ_OK ) {
		Com_WPrintf( "unzGetGlobalInfo() failed on %s\n", packfile );
		goto fail;
	}

	numFiles = zGlobalInfo.number_entry;
	if( numFiles > MAX_FILES_IN_PK2 ) {
		Com_WPrintf( "%s has too many files, %i > %i\n", packfile, numFiles, MAX_FILES_IN_PK2 );
		goto fail;
	}

	if( unzGoToFirstFile( zFile ) != UNZ_OK ) {
		Com_WPrintf( "unzGoToFirstFile() failed on %s\n", packfile );
		goto fail;
	}

	namesLength = 0;
	for( i = 0; i < numFiles; i++ ) {
		if( unzGetCurrentFileInfo( zFile, &zInfo, name, sizeof( name ), NULL, 0, NULL, 0 ) != UNZ_OK ) {
			Com_WPrintf( "unzGetCurrentFileInfo() failed on %s\n", packfile );
			goto fail;
		}

		namesLength += strlen( name ) + 1;

		if( i != numFiles - 1 && unzGoToNextFile( zFile ) != UNZ_OK ) {
			Com_WPrintf( "unzGoToNextFile() failed on %s\n", packfile );

		}
	}

	hashSize = Q_CeilPowerOfTwo( numFiles );
	if( hashSize > 32 ) {
		hashSize >>= 1;
	}

	len = strlen( packfile );
	len = ( len + 3 ) & ~3;
	pack = FS_Malloc( sizeof( pack_t ) +
		numFiles * sizeof( packfile_t ) +
		hashSize * sizeof( packfile_t * ) +
		namesLength + len );
	strcpy( pack->filename, packfile );
	pack->zFile = zFile;
	pack->fp = NULL;
	pack->numfiles = numFiles;
	pack->hashSize = hashSize;
	pack->files = ( packfile_t * )( ( byte * )pack + sizeof( pack_t ) + len );
	pack->fileHash = ( packfile_t ** )( pack->files + numFiles );
	names = ( char * )( pack->fileHash + hashSize );
	memset( pack->fileHash, 0, hashSize * sizeof( packfile_t * ) );

// parse the directory
	unzGoToFirstFile( zFile );

	for( i = 0, file = pack->files; i < numFiles; i++, file++ ) {
		unzGetCurrentFileInfo( zFile, &zInfo, name, sizeof( name ), NULL, 0, NULL, 0 );
		Q_strlwr( name );

		len = strlen( name ) + 1;
		file->name = names;

		strcpy( file->name, name );
		file->filepos = unzGetCurrentFileInfoPosition( zFile );
		file->filelen = zInfo.uncompressed_size;

		hash = Com_HashPath( file->name, hashSize );
		file->hashNext = pack->fileHash[hash];
		pack->fileHash[hash] = file;

		names += len;
		
		if( i != numFiles - 1 ) {
			unzGoToNextFile( zFile );
		}
	}

	FS_DPrintf( "Added zipfile '%s', %d files, %d hash table entries\n",
		packfile, numFiles, hashSize );

	return pack;

fail:
	unzClose( zFile );
	return NULL;
}

static int pakcmp( const void *p1, const void *p2 ) {
	const char *s1 = *( const char ** )p1;
	const char *s2 = *( const char ** )p2;

    if( !strncmp( s1, "pak", 3 ) ) {
        if( strncmp( s2, "pak", 3 ) ) {
            return -1;
        }
    } else if( !strncmp( s2, "pak", 3 ) ) {
        return 1;
    }

	return strcmp( s1, s2 );
}

static void FS_LoadPackFiles( const char *extension, pack_t *(loadfunc)( const char * ) ) {
	int				i;
	searchpath_t	*search;
	pack_t			*pak;
	char **			list;
	int				numFiles;
    char            path[MAX_OSPATH];

	list = Sys_ListFiles( fs_gamedir, extension, FS_SEARCH_NOSORT, &numFiles );
	if( !list ) {
		return;
	}
	qsort( list, numFiles, sizeof( list[0] ), pakcmp );
	for( i = 0; i < numFiles; i++ ) {
        Com_sprintf( path, sizeof( path ), "%s/%s", fs_gamedir, list[i] );
		pak = (*loadfunc)( path );
		if( !pak )
			continue;
		search = FS_Malloc( sizeof( searchpath_t ) );
		search->filename[0] = 0;
		search->pack = pak;
		search->next = fs_searchpaths;
		fs_searchpaths = search;	
	}

	Sys_FreeFileList( list );
		
}

/*
================
FS_AddGameDirectory

Sets fs_gamedir, adds the directory to the head of the path,
then loads and adds pak*.pak, then anything else in alphabethical order.
================
*/
void FS_AddGameDirectory( const char *fmt, ... ) {
    va_list argptr;
	searchpath_t	*search;
	int length;

    va_start( argptr, fmt );
	Q_vsnprintf( fs_gamedir, sizeof( fs_gamedir ), fmt, argptr );
    va_end( argptr );

#ifdef _WIN32
	Com_ReplaceSeparators( fs_gamedir, '/' );
    Q_strlwr( fs_gamedir );
#endif

	//
	// add the directory to the search path
	//
	if( !( fs_restrict_mask->integer & 1 ) ) {
		length = strlen( fs_gamedir );
		search = FS_Malloc( sizeof( searchpath_t ) + length );
		search->pack = NULL;
		strcpy( search->filename, fs_gamedir );
		search->next = fs_searchpaths;
		fs_searchpaths = search;
	}

	//
	// add any pak files in the format *.pak
	//
	if( !( fs_restrict_mask->integer & 2 ) ) {
		FS_LoadPackFiles( ".pak", FS_LoadPakFile );
	}

	//
	// add any zip files in the format *.pk2
	//
	if( !( fs_restrict_mask->integer & 4 ) ) {
		FS_LoadPackFiles( ".pk2", FS_LoadZipFile );
	}

}


/*
=================
FS_GetModList
=================
*/
#define MAX_LISTED_MODS		32

static void FS_GetModList( char **list, int *count ) {
	char path[MAX_OSPATH];
	FILE *fp;
	char **dirlist;
	int numDirs;
	int i;

	if( !( dirlist = Sys_ListFiles( fs_basedir->string, NULL, FS_SEARCHDIRS_ONLY, &numDirs ) ) ) {
		return;
	}

	for( i = 0; i < numDirs; i++ ) {
		if( !strcmp( dirlist[i], BASEDIRNAME ) ) {
			continue;
		}

#ifdef _WIN32
		Com_sprintf( path, sizeof( path ), "%s/%s/gamex86.dll", fs_basedir->string, dirlist[i] );
#else
		Com_sprintf( path, sizeof( path ), "%s/%s/gamei386.so", fs_basedir->string, dirlist[i] );
#endif

		if( !( fp = fopen( path, "rb" ) ) ) {
			continue;
		}
		fclose( fp );

		Com_sprintf( path, sizeof( path ), "%s/%s/description.txt", fs_basedir->string, dirlist[i] );

		if( ( fp = fopen( path, "r" ) ) != NULL ) {
			Q_strncpyz( path, va( "%s\n", dirlist[i] ), sizeof( path ) - MAX_QPATH );
			fgets( path + strlen( path ), MAX_QPATH, fp );
			fclose( fp );
			list[*count] = FS_CopyString( path );
		} else {
			list[*count] = FS_CopyString( dirlist[i] );
		}
		
		if( (*count)++ == MAX_LISTED_MODS ) {
			break;
		}
	}

	Sys_FreeFileList( dirlist );
}

/*
=================
FS_CopyExtraInfo
=================
*/
char *FS_CopyExtraInfo( const char *name, const fsFileInfo_t *info ) {
	char	*out;
	int		length;

	if( !name ) {
		return NULL;
	}

	length = strlen( name ) + 1;
	
	out = FS_Malloc( sizeof( *info ) + length );
	strcpy( out, name );

	memcpy( out + length, info, sizeof( *info ) );

	return out;
}

static int FS_WildCmp_r( const char *filter, const char *string ) {
	switch( *filter ) {
	case '\0':
	case ';':
		return !*string;

	case '*':
		return FS_WildCmp_r( filter + 1, string ) || (*string && FS_WildCmp_r( filter, string + 1 ));

	case '?':
		return *string && FS_WildCmp_r( filter + 1, string + 1 );

	default:
		return ((*filter == *string) || (Q_toupper( *filter ) == Q_toupper( *string ))) && FS_WildCmp_r( filter + 1, string + 1 );
	}
}



int FS_WildCmp( const char *filter, const char *string ) {
	do {
		if( FS_WildCmp_r( filter, string ) ) {
			return 1;
		}
		filter = strchr( filter, ';' );
		if( filter ) filter++;
	} while( filter );

	return 0;
}



/*
=================
FS_ListFiles
=================
*/
char **FS_ListFiles( const char *path, const char *extension,
        uint32 flags, int *numFiles )
{
	searchpath_t *search;
	char *listedFiles[MAX_LISTED_FILES];
	int count, total;
	char buffer[MAX_QPATH];
	char **dirlist;
	int numFilesInDir;
	char **list;
	int i, length;
	char *name, *filename;
	fsFileInfo_t	info;

	if( flags & FS_SEARCH_BYFILTER ) {
		if( !extension ) {
			Com_Error( ERR_FATAL, "FS_ListFiles: NULL filter" );
		}
	}

	if( !path ) {
		path = "";
	}

	count = 0;

	if( numFiles ) {
		*numFiles = 0;
	}

	if( !strcmp( path, "$modlist" ) ) {
		FS_GetModList( listedFiles, &count );
	} else {
		switch( flags & FS_PATH_MASK ) {
		case FS_PATH_BASE:
			search = fs_base_searchpaths;
			break;
		default:
			search = fs_searchpaths;
			break;
		}

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

		for( ; search; search = search->next ) {
			if( search->pack ) {
				if( ( flags & FS_TYPE_MASK ) == FS_TYPE_REAL ) {
					/* don't search in paks */
					continue;
				}

				// TODO: add directory search support for pak files
				if( ( flags & FS_SEARCHDIRS_MASK ) == FS_SEARCHDIRS_ONLY ) {
					continue;
				}
				if( ( flags & FS_PATH_MASK ) == FS_PATH_GAME ) {
					if( fs_searchpaths != fs_base_searchpaths && search == fs_base_searchpaths ) {
						/* consider baseq2 a gamedir if no gamedir loaded */
						break;
					}
				}
				if( flags & FS_SEARCH_BYFILTER ) {
				    for( i = 0; i < search->pack->numfiles; i++ ) {
					    name = search->pack->files[i].name;
                        
						// check path
						filename = name;
						if( *path ) {
							length = strlen( path );
							if( strncmp( name, path, length ) ) {
								continue;
							}
							filename += length + 1;
						}

						// check filter
						if( !FS_WildCmp( extension, filename ) ) {
							continue;
						}

						// copy filename
						if( count == MAX_LISTED_FILES ) {
							break;
						}

						if( !( flags & FS_SEARCH_SAVEPATH ) ) {
							filename = COM_SkipPath( filename );
						}
						if( flags & FS_SEARCH_EXTRAINFO ) {
							info.fileSize = search->pack->files[i].filelen;
							listedFiles[count++] = FS_CopyExtraInfo( filename, &info );
						} else {
							listedFiles[count++] = FS_CopyString( filename );
						}
                    }
                } else {
    				for( i = 0; i < search->pack->numfiles; i++ ) {
	    				name = search->pack->files[i].name;
                        
						// check path
						if( *path ) {
							COM_FilePath( name, buffer, sizeof( buffer ) );
							if( strcmp( path, buffer ) ) {
								continue;
							}
						}

						// check extension
						if( extension && strcmp( extension, COM_FileExtension( name ) ) ) {
							continue;
						}
						
						// copy filename
						if( count == MAX_LISTED_FILES ) {
							break;
						}
						if( !( flags & FS_SEARCH_SAVEPATH ) ) {
							name = COM_SkipPath( name );
						}
						if( flags & FS_SEARCH_EXTRAINFO ) {
							info.fileSize = search->pack->files[i].filelen;
							listedFiles[count++] = FS_CopyExtraInfo( name, &info );
						} else {
							listedFiles[count++] = FS_CopyString( name );
						}
					}
				}
			} else {
				if( ( flags & FS_TYPE_MASK ) == FS_TYPE_PAK ) {
					/* don't search in OS filesystem */
					continue;
				}

				Q_strncpyz( buffer, search->filename, sizeof( buffer ) );
				if( *path ) {
					Q_strcat( buffer, sizeof( buffer ), "/" );
					Q_strcat( buffer, sizeof( buffer ), path );
				}

				if( flags & FS_SEARCH_BYFILTER ) {
					dirlist = Sys_ListFiles( buffer, extension, flags|FS_SEARCH_NOSORT, &numFilesInDir );
				} else {
					dirlist = Sys_ListFiles( buffer, extension, flags|FS_SEARCH_NOSORT, &numFilesInDir );
				}

				if( !dirlist ) {
					continue;
				}

				for( i = 0; i < numFilesInDir; i++ ) {
					if( count == MAX_LISTED_FILES ) {
						break;
					}
					name = dirlist[i];
					if( ( flags & FS_SEARCH_SAVEPATH ) && !( flags & FS_SEARCH_BYFILTER ) ) {
						// skip search path
						name += strlen( search->filename ) + 1;
					}
					if( flags & FS_SEARCH_EXTRAINFO ) {
						listedFiles[count++] = FS_CopyExtraInfo( name, ( fsFileInfo_t * )( dirlist[i] + strlen( dirlist[i] ) + 1 ) );
					} else {
						listedFiles[count++] = FS_CopyString( name );
					}
				}
				Sys_FreeFileList( dirlist );
				
			}
			if( count == MAX_LISTED_FILES ) {
				break;
			}
		}
	}

	if( !count ) {
		return NULL;
	}

	// sort alphabetically (ignoring FS_SEARCH_NOSORT)
	qsort( listedFiles, count, sizeof( listedFiles[0] ), SortStrcmp );

	// remove duplicates
	total = 1;
	for( i = 1; i < count; i++ ) {
		if( !strcmp( listedFiles[i-1], listedFiles[i] ) ) {
			Z_Free( listedFiles[i-1] );
			listedFiles[i-1] = NULL;
		} else {
			total++;
		}
	}

	list = FS_Malloc( sizeof( char * ) * ( total + 1 ) );

	total = 0;
	for( i = 0; i < count; i++ ) {
		if( listedFiles[i] ) {
			list[total++] = listedFiles[i];
		}
	}
	list[total] = NULL;

	if( numFiles ) {
		*numFiles = total;
	}

	return list;


}

/*
=================
FS_FreeFileList
=================
*/
void FS_FreeFileList( char **list ) {
	char **p;

	if( !list ) {
		Com_Error( ERR_FATAL, "FS_FreeFileList: NULL" );
	}

	p = list;
	while( *p ) {
		Z_Free( *p++ );
	}

	Z_Free( list );
}

/*
=================
FS_CopyFile_f

extract file from *.pak, *.pk2 or *.gz
=================
*/
static void FS_CopyFile_f( void ) {
	if( Cmd_Argc() < 2 ) {
		Com_Printf( "Usage: %s <sourcePath> <destPath>\n", Cmd_Argv( 0 ) );
		return;
	}

	if( FS_CopyFile( Cmd_Argv( 1 ), Cmd_Argv( 2 ) ) ) {
		Com_Printf( "File copied successfully\n" );
	} else {
		Com_Printf( "Failed to copy file\n" );
	}
}

/*
============
FS_FDir_f
============
*/
static void FS_FDir_f( void ) {
	char	**dirnames;
	int		ndirs = 0;
	int     i;
    char    *filter;

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

    filter = Cmd_Argv( 1 );

	i = FS_SEARCH_BYFILTER;
	if( Cmd_Argc() > 2 ) {
		i |= FS_SEARCH_SAVEPATH;
	}
    
	if( ( dirnames = FS_ListFiles( NULL, filter, i, &ndirs ) ) != NULL ) {
		for( i = 0; i < ndirs; i++ ) {
			Com_Printf( "%s\n", dirnames[i] );
		}
		FS_FreeFileList( dirnames );
	}
	Com_Printf( "%i files listed\n", ndirs );
	
}

/*
============
FS_Dir_f
============
*/
static void FS_Dir_f( void ) {
	char	**dirnames;
	int		ndirs = 0;
	int     i;
    char    *ext;

	if( Cmd_Argc() < 2 ) {
		Com_Printf( "Usage: %s <directory> [.extension]\n", Cmd_Argv( 0 ) );
		return;
	}
    if( Cmd_Argc() > 2 ) {
        ext = Cmd_Argv( 2 );
    } else {
        ext = NULL;
    }
    dirnames = FS_ListFiles( Cmd_Argv( 1 ), ext, 0, &ndirs );
	if( dirnames ) {
		for( i = 0; i < ndirs; i++ ) {
			Com_Printf( "%s\n", dirnames[i] );
		}
		FS_FreeFileList( dirnames );
	}
	Com_Printf( "%i files listed\n", ndirs );
	
}

/*
============
FS_WhereIs_f
============
*/
static void FS_WhereIs_f( void ) {
    searchpath_t *search;
    pack_t *pak;
    packfile_t *entry;
    uint32 hash;
    char filename[MAX_QPATH];
    char fullpath[MAX_OSPATH];
    char *path;
    fsFileInfo_t info;

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

    Cmd_ArgvBuffer( 1, filename, sizeof( filename ) );
    Q_strlwr( filename );

    path = FS_ExpandLinks( filename );
    if( path != filename ) {
        Com_Printf( "%s linked to %s\n", filename, path );
    }

    hash = Com_HashPath( path, 0 );
    
    for( search = fs_searchpaths; search; search = search->next ) {
	    // is the element a pak file?
		if( search->pack ) {
		    // look through all the pak file elements
			pak = search->pack;
			entry = pak->fileHash[ hash & ( pak->hashSize - 1 ) ];
			for( ; entry; entry = entry->hashNext ) {
				// both entry->name and filename should be already lowercased
				if( !strcmp( entry->name, path ) ) {
                    Com_Printf( "%s/%s (%d bytes)\n", pak->filename,
                            path, entry->filelen );
                    return;
                }
            }
        } else {
            Com_sprintf( fullpath, sizeof( fullpath ), "%s/%s",
                    search->filename, path );
			FS_ConvertToSysPath( fullpath );
            if( Sys_GetFileInfo( fullpath, &info ) ) {
                Com_Printf( "%s/%s (%d bytes)\n", search->filename, filename,
                        info.fileSize );
                return;
            }
        }
        
    }

    Com_Printf( "%s was not found\n", path );
}

/*
============
FS_Path_f
============
*/
void FS_Path_f( void ) {
	searchpath_t	*s;
	int				numFilesInPAK;
	int				numFilesInPK2;

	numFilesInPAK = 0;
	numFilesInPK2 = 0;

	Com_Printf( "Current search path:\n" );
	for( s = fs_searchpaths; s; s = s->next ) {
		if( s->pack ) {
			if( s->pack->zFile ) {
				numFilesInPK2 += s->pack->numfiles;
			} else {
				numFilesInPAK += s->pack->numfiles;
			}
		}

		//if( s == fs_base_searchpaths )
		//	Com_Printf( "----------\n" );
		if( s->pack )
			Com_Printf( "%s (%i files)\n", s->pack->filename, s->pack->numfiles );
		else
			Com_Printf( "%s\n", s->filename );
	}

	if( !( fs_restrict_mask->integer & 2 ) ) {
		Com_Printf( "%i files in PAK files\n", numFilesInPAK );
	}

	if( !( fs_restrict_mask->integer & 4 ) ) {
		Com_Printf( "%i files in PK2 files\n", numFilesInPK2 );
	}

}

/*
================
FS_Stats_f
================
*/
static void FS_Stats_f( void ) {
	searchpath_t *path;
	pack_t *pack, *maxpack = NULL;
	packfile_t *file, *max = NULL;
	int i;
	int len, maxLen = 0;
	int totalHashSize, totalLen;

	totalHashSize = totalLen = 0;
	for( path = fs_searchpaths; path; path = path->next ) {
		if( !( pack = path->pack ) ) {
			continue;
		}
		for( i = 0; i < pack->hashSize; i++ ) {
			if( !( file = pack->fileHash[i] ) ) {
				continue;
			}
			len = 0;
			for( ; file ; file = file->hashNext ) {
				len++;
			}
			if( maxLen < len ) {
				max = pack->fileHash[i];
				maxpack = pack;
				maxLen = len;
			}
			totalLen += len;
			totalHashSize++;
		}
		//totalHashSize += pack->hashSize;
	}

	Com_Printf( "LoadFile counter: %d\n", loadCount );
	Com_Printf( "Static LoadFile counter: %d\n", loadCountStatic );

	if( !totalHashSize ) {
		Com_Printf( "No stats to display\n" );
		return;
	}

	Com_Printf( "Maximum hash bucket length is %d, average is %.2f\n", maxLen, ( float )totalLen / totalHashSize );
	if( max ) {
		Com_Printf( "Dumping longest bucket (%s):\n", maxpack->filename );
		for( file = max; file ; file = file->hashNext ) {
			Com_Printf( "%s\n", file->name );
		}
	}
}

static const char *FS_Link_g( const char *partial, int state ) {
    static int length;
    static fsLink_t *link;
    char *name;

    if( !state ) {
        length = strlen( partial );
        link = fs_links;
    }

	while( link ) {
        name = link->name;
        link = link->next;
		if( !Q_stricmpn( partial, name, length ) ) {
			return name;
		}
	}

    return NULL;
}

static void FS_UnLink_f( void ) {
    fsLink_t *l, *next, **back;
    char *name;

    if( Cmd_CheckParam( "h", "help" ) ) {
usage:
        Com_Printf( "Usage: %s <name>\n"
                "Deletes the specified symbolic link.\n",
                    Cmd_Argv( 0 ) );
        return;
    }

    if( Cmd_CheckParam( "a", "all" ) ) {
        for( l = fs_links; l; l = next ) {
            next = l->next;
            Z_Free( l->target );
            Z_Free( l );
        }
        fs_links = NULL;
        return;
    }

    if( Cmd_Argc() != 2 ) {
        goto usage;
    }

    name = Cmd_Argv( 1 );
    for( l = fs_links, back = &fs_links; l; l = l->next ) {
        if( !Q_stricmp( l->name, name ) ) {
            break;
        }
        back = &l->next;
    }

    if( !l ) {
        Com_Printf( "Symbolic link '%s' does not exist.\n", name );
        return;
    }

    *back = l->next;
    Z_Free( l->target );
    Z_Free( l );
}

static void FS_Link_f( void ) {
    int argc, length, count;
    fsLink_t *l;
    char *name, *target;

    argc = Cmd_Argc();
    if( argc == 1 ) {
        for( l = fs_links, count = 0; l; l = l->next, count++ ) {
            Com_Printf( "%s --> %s\n", l->name, l->target );
        }
        Com_Printf( "------------------\n"
                "%d symbolic links listed.\n", count );
        return;
    }
    if( Cmd_CheckParam( "h", "help" ) || argc != 3 ) {
        Com_Printf( "Usage: %s <name> <target>\n"
                "Creates symbolic link to target with the specified name.\n"
                "Virtual quake paths are accepted.\n"
                "Links are effective only for reading.\n",
                    Cmd_Argv( 0 ) );
        return;
    }

    name = Cmd_Argv( 1 );
    for( l = fs_links; l; l = l->next ) {
        if( !Q_stricmp( l->name, name ) ) {
            break;
        }
    }

    if( !l ) {
        length = strlen( name );
        l = FS_Malloc( sizeof( *l ) + length );
        strcpy( l->name, name );
        l->nameLength = length;
        l->next = fs_links;
        fs_links = l;
    } else {
        Z_Free( l->target );
    }

    target = Cmd_Argv( 2 );
    l->target = FS_CopyString( target );
    l->targetLength = strlen( target );
}

/*
================
FS_NextPath

Allows enumerating all of the directories in the search path
================
*/
char *FS_NextPath (char *prevpath)
{
	searchpath_t	*s;
	char			*prev;

	if (!prevpath)
		return fs_gamedir;

	prev = fs_gamedir;
	for (s=fs_searchpaths ; s ; s=s->next)
	{
		if (s->pack)
			continue;
		if (prevpath == prev)
			return s->filename;
		prev = s->filename;
	}

	return NULL;
}



/*
============
FS_Gamedir

Called to find where to write a file (demos, savegames, etc)
============
*/
char *FS_Gamedir( void ) {
	return fs_gamedir;
}

static void FS_FreeSearchPath( searchpath_t *path ) {
	pack_t *pak;

	if( ( pak = path->pack ) != NULL ) {
		if( pak->zFile ) {
			unzClose( pak->zFile );
		} else {
			fclose( pak->fp );
		}
		Z_Free( pak );
	}

	Z_Free( path );
}

/*
================
FS_Shutdown
================
*/
void FS_Shutdown( qboolean total ) {
	searchpath_t	*path, *next;
    fsLink_t *l, *nextLink;
	fsFile_t *file;
	int i;

	if( !fs_searchpaths ) {
		return;
	}

	if( total ) {
		/* close file handles */
		for( i = 0, file = fs_files; i < MAX_FILE_HANDLES; i++, file++ ) {
			if( file->type != FS_FREE ) {
				Com_WPrintf( "FS_Shutdown: closing handle %i: '%s'\n",
                        i + 1, file->name );
				FS_FCloseFile( i + 1 );
			}
		}

        /* free symbolic links */
        for( l = fs_links; l; l = nextLink ) {
            nextLink = l->next;
            Z_Free( l->target );
            Z_Free( l );
        }

        fs_links = NULL;
	}

	/* free search paths */
	for( path = fs_searchpaths; path; path = next ) {
		next = path->next;
		FS_FreeSearchPath( path );
	}

	fs_searchpaths = NULL;

	FS_FlushCache();

    if( total ) {
    	Z_LeakTest( TAG_FILESYSTEM );
    }

	Cmd_RemoveCommand( "path" );
	Cmd_RemoveCommand( "fdir" );
	Cmd_RemoveCommand( "dir" );
	Cmd_RemoveCommand( "copyfile" );
	Cmd_RemoveCommand( "fs_stats" );
	Cmd_RemoveCommand( "link" );
	Cmd_RemoveCommand( "unlink" );
}

/*
================
FS_DefaultGamedir
================
*/
static void FS_DefaultGamedir( void ) {
#ifdef __unix__
    char *homedir = getenv( "HOME" );

    if( homedir ) {
    	FS_AddGameDirectory( "%s/"HOMEDIRNAME"/"BASEDIRNAME, homedir );
    } else
#else
    {
		/* write to baseq2pro on Windows */
    	Com_sprintf( fs_gamedir, sizeof( fs_gamedir ), "%s/"INITDIRNAME,
            fs_basedir->string );
    }
#endif

	Cvar_Set( "game", "" );
	Cvar_Set( "gamedir", "" );
}


/*
================
FS_SetupGamedir

Sets the gamedir and path to a different directory.
================
*/
static void FS_SetupGamedir( void ) {
	fs_game = Cvar_Get( "game", "", CVAR_LATCHED|CVAR_SERVERINFO );
	fs_game->subsystem = CVAR_SYSTEM_FILES;

	if( !fs_game->string[0] || !FS_strcmp( fs_game->string, BASEDIRNAME ) ||
		!FS_strcmp( fs_game->string, INITDIRNAME ) )
	{
		FS_DefaultGamedir();
		return;
	}

	if( !FS_ValidatePath( fs_game->string ) || strchr( fs_game->string, '/' ) ||
		strchr( fs_game->string, '\\' ) || strchr( fs_game->string, ':' ) )
	{
		Com_WPrintf( "Gamedir should be a single filename, not a path. "
            "Falling back to default.\n" );
		FS_DefaultGamedir();
		return;
	}

	// this one is left for compatibility with server browsers, etc
	Cvar_FullSet( "gamedir", fs_game->string, CVAR_ROM|CVAR_SERVERINFO,
        CVAR_SET_DIRECT );

	if( fs_cddir->string[0] )
		FS_AddGameDirectory( "%s/%s", fs_cddir->string, fs_game->string );

	FS_AddGameDirectory( "%s/%s", fs_basedir->string, fs_game->string );

#ifdef __unix__
    {
        char *homedir = getenv( "HOME" );

        if( homedir ) {
            FS_AddGameDirectory( "%s/"HOMEDIRNAME"/%s",
                homedir, fs_game->string );
        }
    }
#endif
}

qboolean FS_SafeToRestart( void ) {
	fsFile_t	*file;
	int			i;
    
	/* make sure no files are opened for reading */
	for( i = 0, file = fs_files; i < MAX_FILE_HANDLES; i++, file++ ) {
		if( file->type == FS_FREE ) {
			continue;
		}
		if( file->mode == FS_MODE_READ ) {
            return qfalse;
		}
    }

    return qtrue;
}

/*
================
FS_Restart
================
*/
void FS_Restart( void ) {
	fsFile_t	*file;
	int			i;
	fileHandle_t temp;
	searchpath_t *path, *next;
	
	Com_Printf( "---------- FS_Restart ----------\n" );
	
	/* temporary disable logfile */
	temp = com_logFile;
	com_logFile = 0;

	/* make sure no files are opened for reading */
	for( i = 0, file = fs_files; i < MAX_FILE_HANDLES; i++, file++ ) {
		if( file->type == FS_FREE ) {
			continue;
		}
		if( file->mode == FS_MODE_READ ) {
			Com_Error( ERR_FATAL, "FS_Restart: closing handle %i: %s",
                i + 1, file->name );
		}
	}
	
	if( fs_restrict_mask->latched_string ) {
		/* perform full reset */
		FS_Shutdown( qfalse );
		FS_Init();
	} else {
		/* just change gamedir */	
		for( path = fs_searchpaths; path != fs_base_searchpaths; path = next ) {
			next = path->next;
			FS_FreeSearchPath( path );
		}

		fs_searchpaths = fs_base_searchpaths;

		FS_SetupGamedir();
		FS_Path_f();
	}

	/* re-enable logfile */
	com_logFile = temp;
	
	Com_Printf( "--------------------------------\n" );
}

/*
================
FS_FillAPI
================
*/
void FS_FillAPI( fsAPI_t *api ) {
	api->LoadFile = FS_LoadFile;
	api->LoadFileEx = FS_LoadFileEx;
	api->FreeFile = FS_FreeFile;
	api->FOpenFile = FS_FOpenFile;
	api->FCloseFile = FS_FCloseFile;
	api->Tell = FS_Tell;
	api->RawTell = FS_RawTell;
	api->Read = FS_Read;
	api->Write = FS_Write;
	api->ListFiles = FS_ListFiles;
	api->FreeFileList = FS_FreeFileList;
}

/*
================
FS_Init
================
*/
void FS_Init( void ) {
	int	startTime, i;
    char *base;

	startTime = Sys_Milliseconds();

	Com_Printf( "---------- FS_Init ----------\n" );

	Cmd_AddCommand( "path", FS_Path_f );
	Cmd_AddCommand( "fdir", FS_FDir_f );
	Cmd_AddCommand( "dir", FS_Dir_f );
	Cmd_AddCommand( "copyfile", FS_CopyFile_f );
	Cmd_AddCommand( "fs_stats", FS_Stats_f );
	Cmd_AddCommand( "whereis", FS_WhereIs_f );
	Cmd_AddCommandEx( "link", FS_Link_f, FS_Link_g );
	Cmd_AddCommandEx( "unlink", FS_UnLink_f, FS_Link_g );

	fs_debug = Cvar_Get( "fs_debug", "0", 0 );
#ifdef _WIN32_WCE
	fs_restrict_mask = Cvar_Get( "fs_restrict_mask", "0", CVAR_NOSET );
#else
	fs_restrict_mask = Cvar_Get( "fs_restrict_mask", "4", CVAR_NOSET );
#endif
	fs_restrict_mask->subsystem = CVAR_SYSTEM_FILES;

	if( ( fs_restrict_mask->integer & 7 ) == 7 ) {
		Com_WPrintf( "Invalid fs_restrict_mask value %d. "
            "Falling back to default.\n",
			    fs_restrict_mask->integer );
		Cvar_SetInteger( "fs_restrict_mask", 4 );
	}
	

	//
	// basedir <path>
	// allows the game to run from outside the data tree
	//

#ifdef __unix__
    base = DATADIR;
#else
    base = Sys_GetCurrentDirectory();
#endif

	fs_basedir = Cvar_Get( "basedir", base, CVAR_NOSET );
	
	/* strip trailing slash */
	if( fs_basedir->string[0] ) {
		i = strlen( fs_basedir->string ) - 1;
		if( fs_basedir->string[i] == '/' ||
		    fs_basedir->string[i] == '\\' )
		{
			fs_basedir->string[i] = 0;
		}
	}

	//
	// cddir <path>
	// Logically concatenates the cddir after the basedir 
	// allows the game to run from outside the data tree
	//
	fs_cddir = Cvar_Get( "cddir", "", CVAR_NOSET );
	if( fs_cddir->string[0] ) {
		i = strlen( fs_cddir->string ) - 1;
		if( fs_cddir->string[i] == '/' ||
		    fs_cddir->string[i] == '\\' )
		{
			fs_cddir->string[i] = 0;
		}
		FS_AddGameDirectory( "%s/"BASEDIRNAME, fs_cddir->string );
	}

	//
	// start up with baseq2 by default
	//
	FS_AddGameDirectory( "%s/"BASEDIRNAME, fs_basedir->string );
	FS_AddGameDirectory( "%s/"INITDIRNAME, fs_basedir->string );

	fs_base_searchpaths = fs_searchpaths;

	//
	// check for game override
	//

	FS_SetupGamedir();

	FS_Path_f();

	FS_FillAPI( &fs );
	
	Com_Printf( "%i msec to init filesystem\n", Sys_Milliseconds() - startTime );
	Com_Printf( "-----------------------------\n" );
}

/*
================
FS_NeedRestart
================
*/
qboolean FS_NeedRestart( void ) {
	if( fs_game->latched_string || fs_restrict_mask->latched_string ) {
		return qtrue;
	}
	
	return qfalse;
}


// TODO: remove it
int	Developer_searchpath( int who ) {
	if( !strcmp( fs_game->string, "xatrix" ) ) {
		return 1;
	}

	if( !strcmp( fs_game->string, "rogue" ) ) {
		return 1;
	}

	return 0;


}





