#include "quakedef.h"
#include "fs.h"

#ifdef AVAIL_ZLIB

#ifndef ZEXPORT
	#define ZEXPORT VARGS
#endif
#include <zlib.h>

typedef struct {
	unsigned char ident1;
	unsigned char ident2;
	unsigned char cm;
	unsigned char flags;
	unsigned int mtime;
	unsigned char xflags;
	unsigned char os;
} gzheader_t;
#define sizeofgzheader_t 10

#define	GZ_FTEXT	1
#define	GZ_FHCRC	2
#define GZ_FEXTRA	4
#define GZ_FNAME	8
#define GZ_FCOMMENT	16
#define GZ_RESERVED (32|64|128)

#ifdef DYNAMIC_ZLIB
#define ZLIB_LOADED() (zlib_handle!=NULL)
void *zlib_handle;
#define ZSTATIC(n)
#else
#define ZLIB_LOADED() 1
#define ZSTATIC(n) = &n

#ifdef _MSC_VER
# ifdef _WIN64
# pragma comment(lib, MSVCLIBSPATH "zlib64.lib")
# else
# pragma comment(lib, MSVCLIBSPATH "zlib.lib")
# endif
#endif
#endif

//#pragma comment(lib, MSVCLIBSPATH "zlib.lib")

static int (ZEXPORT *qinflateEnd) (z_streamp strm) ZSTATIC(inflateEnd);
static int (ZEXPORT *qinflate) (z_streamp strm, int flush) ZSTATIC(inflate);
static int (ZEXPORT *qinflateInit2_) (z_streamp strm, int  windowBits,
                                      const char *version, int stream_size) ZSTATIC(inflateInit2_);
static uLong (ZEXPORT *qcrc32)   (uLong crc, const Bytef *buf, uInt len) ZSTATIC(crc32);

#define qinflateInit2(strm, windowBits) \
        qinflateInit2_((strm), (windowBits), ZLIB_VERSION, sizeof(z_stream))

qboolean LibZ_Init(void)
{
	#ifdef DYNAMIC_ZLIB
	static dllfunction_t funcs[] =
	{
		{(void*)&qinflateEnd,		"inflateEnd"},
		{(void*)&qinflate,			"inflate"},
		{(void*)&qinflateInit2_,	"inflateInit2_"},
		{(void*)&qcrc32,			"crc32"},
		{NULL, NULL}
	};
	if (!ZLIB_LOADED())
		zlib_handle = Sys_LoadLibrary("zlib1", funcs);
	#endif
	return ZLIB_LOADED();
}

//outfile may be null
vfsfile_t *FS_DecompressGZip(vfsfile_t *infile, vfsfile_t *outfile)
{
	char inchar;
	unsigned short inshort;
	vfsfile_t *temp;
	gzheader_t header;

	if (VFS_READ(infile, &header, sizeofgzheader_t) == sizeofgzheader_t)
	{
		if (header.ident1 != 0x1f || header.ident2 != 0x8b || header.cm != 8 || header.flags & GZ_RESERVED)
		{
			VFS_SEEK(infile, 0);
			return infile;
		}
	}
	else
	{
		VFS_SEEK(infile, 0);
		return infile;
	}
	if (header.flags & GZ_FEXTRA)
	{
		VFS_READ(infile, &inshort, sizeof(inshort));
		inshort = LittleShort(inshort);
		VFS_SEEK(infile, VFS_TELL(infile) + inshort);
	}

	if (header.flags & GZ_FNAME)
	{
		Con_Printf("gzipped file name: ");
		do {
			if (VFS_READ(infile, &inchar, sizeof(inchar)) != 1)
				break;
			Con_Printf("%c", inchar);
		} while(inchar);
		Con_Printf("\n");
	}

	if (header.flags & GZ_FCOMMENT)
	{
		Con_Printf("gzipped file comment: ");
		do {
			if (VFS_READ(infile, &inchar, sizeof(inchar)) != 1)
				break;
			Con_Printf("%c", inchar);
		} while(inchar);
		Con_Printf("\n");
	}

	if (header.flags & GZ_FHCRC)
	{
		VFS_READ(infile, &inshort, sizeof(inshort));
	}



	if (outfile)
		temp = outfile;
	else
	{
		temp = FS_OpenTemp();
		if (!temp)
		{
			VFS_SEEK(infile, 0);	//doh
			return infile;
		}
	}


	{
		unsigned char inbuffer[16384];
		unsigned char outbuffer[16384];
		int ret;

		z_stream strm = {
			inbuffer,
			0,
			0,

			outbuffer,
			sizeof(outbuffer),
			0,

			NULL,
			NULL,

			NULL,
			NULL,
			NULL,

			Z_UNKNOWN,
			0,
			0
		};

		strm.avail_in = VFS_READ(infile, inbuffer, sizeof(inbuffer));
		strm.next_in = inbuffer;

		qinflateInit2(&strm, -MAX_WBITS);

		while ((ret=qinflate(&strm, Z_SYNC_FLUSH)) != Z_STREAM_END)
		{
			if (strm.avail_in == 0 || strm.avail_out == 0)
			{
				if (strm.avail_in == 0)
				{
					strm.avail_in = VFS_READ(infile, inbuffer, sizeof(inbuffer));
					strm.next_in = inbuffer;
					if (!strm.avail_in)
						break;
				}

				if (strm.avail_out == 0)
				{
					strm.next_out = outbuffer;
					VFS_WRITE(temp, outbuffer, strm.total_out);
					strm.total_out = 0;
					strm.avail_out = sizeof(outbuffer);
				}
				continue;
			}

			//doh, it terminated for no reason
			if (ret != Z_STREAM_END)
			{
				qinflateEnd(&strm);
				Con_Printf("Couldn't decompress gz file\n");
				VFS_CLOSE(temp);
				VFS_CLOSE(infile);
				return NULL;
			}
		}
		//we got to the end
		VFS_WRITE(temp, outbuffer, strm.total_out);

		qinflateEnd(&strm);

		VFS_SEEK(temp, 0);
	}
	VFS_CLOSE(infile);

	return temp;
}

#include "unzip.c"

typedef struct
{
	fsbucket_t bucket;
	char	name[MAX_QPATH];
	int		filepos, filelen;
} zpackfile_t;


typedef struct zipfile_s
{
	searchpathfuncs_t pub;

	char filename[MAX_OSPATH];
	unzFile handle;
	int		numfiles;
	zpackfile_t	*files;

#ifdef HASH_FILESYSTEM
	hashtable_t hash;
#endif

	vfsfile_t *raw;
	vfsfile_t *currentfile;	//our unzip.c can only handle one active file at any one time
							//so we have to keep closing and switching.
							//slow, but it works. most of the time we'll only have a single file open anyway.
	int references;	//and a reference count
} zipfile_t;


static void QDECL FSZIP_GetPathDetails(searchpathfuncs_t *handle, char *out, unsigned int outlen)
{
	zipfile_t *zip = (void*)handle;

	if (zip->references != 1)
		Q_snprintfz(out, outlen, "(%i)", zip->references-1);
	else
		*out = '\0';
}
static void QDECL FSZIP_ClosePath(searchpathfuncs_t *handle)
{
	zipfile_t *zip = (void*)handle;

	if (--zip->references > 0)
		return;	//not yet time

	unzClose(zip->handle);
	if (zip->files)
		Z_Free(zip->files);
	Z_Free(zip);
}
static void QDECL FSZIP_BuildHash(searchpathfuncs_t *handle, int depth, void (QDECL *AddFileHash)(int depth, const char *fname, fsbucket_t *filehandle, void *pathhandle))
{
	zipfile_t *zip = (void*)handle;
	int i;

	for (i = 0; i < zip->numfiles; i++)
	{
		AddFileHash(depth, zip->files[i].name, &zip->files[i].bucket, &zip->files[i]);
	}
}
static qboolean QDECL FSZIP_FLocate(searchpathfuncs_t *handle, flocation_t *loc, const char *filename, void *hashedresult)
{
	zpackfile_t *pf = hashedresult;
	int i;
	zipfile_t	*zip = (void*)handle;

// look through all the pak file elements

	if (pf)
	{	//is this a pointer to a file in this pak?
		if (pf < zip->files || pf >= zip->files + zip->numfiles)
			return false;	//was found in a different path
	}
	else
	{
		for (i=0 ; i<zip->numfiles ; i++)	//look for the file
		{
			if (!stricmp (zip->files[i].name, filename))
			{
				pf = &zip->files[i];
				break;
			}
		}
	}

	if (pf)
	{
		if (loc)
		{
			loc->index = pf - zip->files;
			strcpy(loc->rawname, zip->filename);
			loc->offset = pf->filepos;
			loc->len = pf->filelen;

			unzLocateFileMy (zip->handle, loc->index, zip->files[loc->index].filepos);
			loc->offset = unzGetCurrentFileUncompressedPos(zip->handle);
//			if (loc->offset<0)
//			{	//file not found, or is compressed.
//				*loc->rawname = '\0';
//				loc->offset=0;
//			}
		}
		return true;
	}
	return false;
}

static void QDECL FSZIP_ReadFile(searchpathfuncs_t *handle, flocation_t *loc, char *buffer)
{
	zipfile_t *zip = (void*)handle;
	int err;

	unzLocateFileMy (zip->handle, loc->index, zip->files[loc->index].filepos);

	unzOpenCurrentFile (zip->handle);
	err = unzReadCurrentFile (zip->handle, buffer, zip->files[loc->index].filelen);
	unzCloseCurrentFile (zip->handle);

	if (err!=zip->files[loc->index].filelen)
	{
		Con_Printf ("Can't extract file \"%s:%s\" (corrupt)\n", zip->filename, zip->files[loc->index].name);
		return;
	}
	return;
}

static int QDECL FSZIP_EnumerateFiles (searchpathfuncs_t *handle, const char *match, int (QDECL *func)(const char *, int, void *, searchpathfuncs_t *spath), void *parm)
{
	zipfile_t *zip = (void*)handle;
	int		num;

	for (num = 0; num<(int)zip->numfiles; num++)
	{
		if (wildcmp(match, zip->files[num].name))
		{
			if (!func(zip->files[num].name, zip->files[num].filelen, parm, &zip->pub))
				return false;
		}
	}

	return true;
}

static int QDECL FSZIP_GeneratePureCRC(searchpathfuncs_t *handle, int seed, int crctype)
{
	zipfile_t *zip = (void*)handle;
	unz_file_info	file_info;

	int result;
	int *filecrcs;
	int numcrcs=0;
	int i;

	filecrcs = BZ_Malloc((zip->numfiles+1)*sizeof(int));
	filecrcs[numcrcs++] = seed;

	unzGoToFirstFile(zip->handle);
	for (i = 0; i < zip->numfiles; i++)
	{
		if (zip->files[i].filelen>0)
		{
			unzGetCurrentFileInfo (zip->handle, &file_info, NULL, 0, NULL, 0, NULL, 0);
			filecrcs[numcrcs++] = file_info.crc;
		}
		unzGoToNextFile (zip->handle);
	}

	if (crctype)
		result = Com_BlockChecksum(filecrcs, numcrcs*sizeof(int));
	else
		result = Com_BlockChecksum(filecrcs+1, (numcrcs-1)*sizeof(int));

	BZ_Free(filecrcs);
	return result;
}

typedef struct {
	vfsfile_t funcs;

	vfsfile_t *defer;

	//in case we're forced away.
	zipfile_t *parent;
	qboolean iscompressed;
	int pos;
	int length;	//try and optimise some things
	int index;
	int startpos;
} vfszip_t;
static qboolean VFSZIP_MakeActive(vfszip_t *vfsz)
{
	int i;
	char buffer[8192];	//must be power of two

	if ((vfszip_t*)vfsz->parent->currentfile == vfsz)
		return true;	//already us
	if (vfsz->parent->currentfile)
		unzCloseCurrentFile(vfsz->parent->handle);

	unzLocateFileMy(vfsz->parent->handle, vfsz->index, vfsz->startpos);
	if (unzOpenCurrentFile(vfsz->parent->handle) == UNZ_BADZIPFILE)
	{
		unz_file_info	file_info;
		buffer[0] = '?';
		buffer[1] = 0;
		if (unzGetCurrentFileInfo (vfsz->parent->handle, &file_info, buffer, sizeof(buffer), NULL, 0, NULL, 0) != UNZ_OK)
			Con_Printf("Zip Error\n");
		if (file_info.compression_method && file_info.compression_method != Z_DEFLATED)
			Con_Printf("unsupported compression method on %s/%s\n", vfsz->parent->filename, buffer);
		else
			Con_Printf("corrupt file within zip, %s/%s\n", vfsz->parent->filename, buffer);
		vfsz->parent->currentfile = NULL;
		return false;
	}


	if (vfsz->pos > 0)
	{
		Con_DPrintf("VFSZIP_MakeActive: Shockingly inefficient\n");

		//now we need to seek up to where we had previously gotten to.
		for (i = 0; i < vfsz->pos-sizeof(buffer); i++)
			unzReadCurrentFile(vfsz->parent->handle, buffer, sizeof(buffer));
		unzReadCurrentFile(vfsz->parent->handle, buffer, vfsz->pos - i);
	}

	vfsz->parent->currentfile = (vfsfile_t*)vfsz;
	return true;
}

static int QDECL VFSZIP_ReadBytes (struct vfsfile_s *file, void *buffer, int bytestoread)
{
	int read;
	vfszip_t *vfsz = (vfszip_t*)file;

	if (vfsz->defer)
		return VFS_READ(vfsz->defer, buffer, bytestoread);

	if (vfsz->iscompressed)
	{
		if (!VFSZIP_MakeActive(vfsz))
			return 0;
		read = unzReadCurrentFile(vfsz->parent->handle, buffer, bytestoread);
	}
	else
	{
		if (vfsz->parent->currentfile != file)
		{
			unzCloseCurrentFile(vfsz->parent->handle);
			VFS_SEEK(vfsz->parent->raw, vfsz->pos+vfsz->startpos);
			vfsz->parent->currentfile = file;
		}
		if (vfsz->pos + bytestoread > vfsz->length)
			bytestoread = max(0, vfsz->length - vfsz->pos);
		read = VFS_READ(vfsz->parent->raw, buffer, bytestoread);
	}

	vfsz->pos += read;
	return read;
}
//static int QDECL VFSZIP_WriteBytes (struct vfsfile_s *file, void *buffer, int bytestoread)
//{
//	Sys_Error("VFSZIP_WriteBytes: Not supported\n");
//	return 0;
//}
static qboolean QDECL VFSZIP_Seek (struct vfsfile_s *file, unsigned long pos)
{
	vfszip_t *vfsz = (vfszip_t*)file;

	if (vfsz->defer)
		return VFS_SEEK(vfsz->defer, pos);

	//This is *really* inefficient
	if (vfsz->iscompressed)
	{	//if they're going to seek on a file in a zip, let's just copy it out
		char buffer[8192];
		unsigned int chunk;
		unsigned int i;
		unsigned int length;

		vfsz->defer = FS_OpenTemp();
		if (vfsz->defer)
		{
			if (vfsz->pos)
			{
				unzCloseCurrentFile(vfsz->parent->handle);
				vfsz->parent->currentfile = NULL;	//make it not us
			}

			length = vfsz->length;
			i = 0;
			vfsz->pos = 0;
			if (!VFSZIP_MakeActive(vfsz))
			{
				/*shouldn't really happen*/
				VFS_CLOSE(vfsz->defer);
				vfsz->defer = NULL;
				return false;
			}

			while (1)
			{
				chunk = length - i;
				if (chunk > sizeof(buffer))
					chunk = sizeof(buffer);
				if (chunk == 0)
					break;
				unzReadCurrentFile(vfsz->parent->handle, buffer, chunk);
				VFS_WRITE(vfsz->defer, buffer, chunk);

				i += chunk;
			}
		}

		unzCloseCurrentFile(vfsz->parent->handle);
		vfsz->parent->currentfile = NULL;	//make it not us

		if (vfsz->defer)
			return VFS_SEEK(vfsz->defer, pos);
		else
		{
			unzCloseCurrentFile(vfsz->parent->handle);
			vfsz->parent->currentfile = NULL;	//make it not us, so the next read starts at the right place
		}
	}
	else
	{
		vfsz->parent->currentfile = NULL;
	}

	if (pos < 0 || pos > vfsz->length)
		return false;
	vfsz->pos = pos;

	return true;
}
static unsigned long QDECL VFSZIP_Tell (struct vfsfile_s *file)
{
	vfszip_t *vfsz = (vfszip_t*)file;

	if (vfsz->defer)
		return VFS_TELL(vfsz->defer);

	return vfsz->pos;
}
static unsigned long QDECL VFSZIP_GetLen (struct vfsfile_s *file)
{
	vfszip_t *vfsz = (vfszip_t*)file;
	return vfsz->length;
}
static void QDECL VFSZIP_Close (struct vfsfile_s *file)
{
	vfszip_t *vfsz = (vfszip_t*)file;

	if (vfsz->parent->currentfile == file)
		vfsz->parent->currentfile = NULL;	//make it not us

	if (vfsz->defer)
		VFS_CLOSE(vfsz->defer);

	FSZIP_ClosePath(&vfsz->parent->pub);
	Z_Free(vfsz);
}

static vfsfile_t *QDECL FSZIP_OpenVFS(searchpathfuncs_t *handle, flocation_t *loc, const char *mode)
{
	int rawofs;
	zipfile_t *zip = (void*)handle;
	vfszip_t *vfsz;

	if (strcmp(mode, "rb"))
		return NULL; //urm, unable to write/append

	if (loc->len < 0)
		return NULL;

	vfsz = Z_Malloc(sizeof(vfszip_t));

	vfsz->parent = zip;
	vfsz->index = loc->index;
	vfsz->startpos = zip->files[loc->index].filepos;
	vfsz->length = loc->len;

#ifdef _DEBUG
	Q_strncpyz(vfsz->funcs.dbgname, zip->files[loc->index].name, sizeof(vfsz->funcs.dbgname));
#endif
	vfsz->funcs.Close = VFSZIP_Close;
	vfsz->funcs.GetLen = VFSZIP_GetLen;
	vfsz->funcs.ReadBytes = VFSZIP_ReadBytes;
	//vfsz->funcs.WriteBytes = VFSZIP_WriteBytes;
	vfsz->funcs.Seek = VFSZIP_Seek;
	vfsz->funcs.Tell = VFSZIP_Tell;
	vfsz->funcs.WriteBytes = NULL;
	vfsz->funcs.seekingisabadplan = true;

	unzLocateFileMy(vfsz->parent->handle, vfsz->index, vfsz->startpos);
	rawofs = unzGetCurrentFileUncompressedPos(zip->handle);
	vfsz->iscompressed = rawofs<0;
	if (!vfsz->iscompressed)
	{
		vfsz->startpos = rawofs;
		VFS_SEEK(zip->raw, vfsz->startpos);
		vfsz->parent->currentfile = (vfsfile_t*)vfsz;
	}
	else if (!ZLIB_LOADED())
	{
		Z_Free(vfsz);
		return NULL;
	}
	else if (!VFSZIP_MakeActive(vfsz))	/*this is called purely as a test*/
	{
		/*
		windows explorer tends to use deflate64 on large files, which zlib and thus we, do not support, thus this is a 'common' failure path
		this might also trigger from other errors, of course.
		*/
		Z_Free(vfsz);
		return NULL;
	}

	zip->references++;

	return (vfsfile_t*)vfsz;
}

/*
=================
COM_LoadZipFile

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.
=================
*/
searchpathfuncs_t *QDECL FSZIP_LoadArchive (vfsfile_t *packhandle, const char *desc)
{
	int i;
	int nextfileziphandle;

	zipfile_t *zip;
	zpackfile_t		*newfiles;

	unz_global_info	globalinf = {0};
	unz_file_info	file_info;

	zip = Z_Malloc(sizeof(zipfile_t));
	Q_strncpyz(zip->filename, desc, sizeof(zip->filename));
	zip->handle = unzOpen ((zip->raw = packhandle));
	if (!zip->handle)
	{
		Z_Free(zip);
		Con_TPrintf (TL_COULDNTOPENZIP, desc);
		return NULL;
	}

	unzGetGlobalInfo (zip->handle, &globalinf);

	zip->numfiles = globalinf.number_entry;

	zip->files = newfiles = Z_Malloc (zip->numfiles * sizeof(zpackfile_t));
	for (i = 0; i < zip->numfiles; i++)
	{
		if (unzGetCurrentFileInfo (zip->handle, &file_info, newfiles[i].name, sizeof(newfiles[i].name), NULL, 0, NULL, 0) != UNZ_OK)
			Con_Printf("Zip Error\n");
		Q_strlwr(newfiles[i].name);
		if (!*newfiles[i].name || newfiles[i].name[strlen(newfiles[i].name)-1] == '/')
			newfiles[i].filelen = -1;
		else
			newfiles[i].filelen = file_info.uncompressed_size;
		newfiles[i].filepos = file_info.c_offset;

		nextfileziphandle = unzGoToNextFile (zip->handle);
		if (nextfileziphandle == UNZ_END_OF_LIST_OF_FILE)
			break;
		else if (nextfileziphandle != UNZ_OK)
			Con_Printf("Zip Error\n");
	}

	zip->references = 1;
	zip->currentfile = NULL;

	Con_TPrintf (TL_ADDEDZIPFILE, desc, zip->numfiles);

	zip->pub.fsver				= FSVER;
	zip->pub.GetPathDetails		= FSZIP_GetPathDetails;
	zip->pub.ClosePath			= FSZIP_ClosePath;
	zip->pub.BuildHash			= FSZIP_BuildHash;
	zip->pub.FindFile			= FSZIP_FLocate;
	zip->pub.ReadFile			= FSZIP_ReadFile;
	zip->pub.EnumerateFiles		= FSZIP_EnumerateFiles;
	zip->pub.GeneratePureCRC	= FSZIP_GeneratePureCRC;
	zip->pub.OpenVFS			= FSZIP_OpenVFS;
	return &zip->pub;
}

#endif