#include "Cafe/OS/common/OSCommon.h"
#include "zlib125.h"
#include "zlib.h"

typedef struct {
	/* +0x00 */ MEMPTR<uint8>		next_in;     /* next input byte */
	/* +0x04 */ uint32be			avail_in;  /* number of bytes available at next_in */
	/* +0x08 */ uint32be			total_in;  /* total number of input bytes read so far */

	/* +0x0C */ MEMPTR<uint8>		next_out; /* next output byte should be put there */
	/* +0x10 */ uint32be			avail_out; /* remaining free space at next_out */
	/* +0x14 */ uint32be			total_out; /* total number of bytes output so far */

	/* +0x18 */ MEMPTR<char>		msg;  /* last error message, NULL if no error */
	/* +0x1C */ MEMPTR<void>		state; /* not visible by applications */

	/* +0x20 */ MEMPTR<void>		zalloc;  /* used to allocate the internal state */
	/* +0x24 */ MEMPTR<void>		zfree;   /* used to free the internal state */
	/* +0x28 */ MEMPTR<void>		opaque;  /* private data object passed to zalloc and zfree */

	/* +0x2C */ uint32be			data_type;  /* best guess about the data type: binary or text */
	/* +0x30 */ uint32be			adler;      /* adler32 value of the uncompressed data */
	/* +0x34 */ uint32be			reserved;   /* reserved for future use */
}z_stream_ppc2;

static_assert(sizeof(z_stream_ppc2) == 0x38);

voidpf zcallocWrapper(voidpf opaque, uInt items, uInt size)
{
	z_stream_ppc2* zstream = (z_stream_ppc2*)opaque;
	PPCInterpreter_t* hCPU = PPCInterpreter_getCurrentInstance();
	hCPU->gpr[3] = zstream->opaque.GetMPTR();
	hCPU->gpr[4] = items;
	hCPU->gpr[5] = size;
	PPCCore_executeCallbackInternal(zstream->zalloc.GetMPTR());
	memset(memory_getPointerFromVirtualOffset(hCPU->gpr[3]), 0, items*size);
	return memory_getPointerFromVirtualOffset(hCPU->gpr[3]);
}

void zcfreeWrapper(voidpf opaque, voidpf baseIndex)
{
	z_stream_ppc2* zstream = (z_stream_ppc2*)opaque;
	PPCInterpreter_t* hCPU = PPCInterpreter_getCurrentInstance();
	hCPU->gpr[3] = zstream->opaque.GetMPTR();
	hCPU->gpr[4] = memory_getVirtualOffsetFromPointer(baseIndex);
	PPCCore_executeCallbackInternal(zstream->zfree.GetMPTR());
	return;
}

void zlib125_zcalloc(PPCInterpreter_t* hCPU)
{
	ppcDefineParamU32(opaque, 0);
	ppcDefineParamU32(items, 1);
	ppcDefineParamU32(size, 2);

	hCPU->gpr[3] = items*size;
	hCPU->instructionPointer = gCoreinitData->MEMAllocFromDefaultHeap.GetMPTR();
}

void zlib125_zcfree(PPCInterpreter_t* hCPU)
{
	ppcDefineParamU32(opaque, 0);
	ppcDefineParamMPTR(baseIndex, 1);

	hCPU->gpr[3] = baseIndex;
	hCPU->instructionPointer = gCoreinitData->MEMFreeToDefaultHeap.GetMPTR();
}

void zlib125_setupHostZStream(z_stream_ppc2* input, z_stream* output, bool fixInternalStreamPtr = true)
{
	output->next_in = input->next_in.GetPtr();
	output->avail_in = (uint32)input->avail_in;
	output->total_in = (uint32)input->total_in;

	output->next_out = input->next_out.GetPtr();
	output->avail_out = (uint32)input->avail_out;
	output->total_out = (uint32)input->total_out;

	output->msg = input->msg.GetPtr();
	output->state = (internal_state*)input->state.GetPtr();

	output->zalloc = zcallocWrapper;
	output->zfree = zcfreeWrapper;
	output->opaque = (void*)input;

	output->data_type = input->data_type;
	output->adler = (uint32)input->adler;
	output->reserved = (uint32)input->reserved;

	if (fixInternalStreamPtr && output->state)
	{
		// in newer zLib versions the internal state has a pointer to the stream at the beginning, we have to update it manually
		// todo - find better solution
		(*(void**)(output->state)) = output;
	}
}

void zlib125_setupUpdateZStream(z_stream* input, z_stream_ppc2* output)
{
	output->next_in = input->next_in;
	output->avail_in = (uint32)input->avail_in;
	output->total_in = (uint32)input->total_in;

	output->next_out = input->next_out;
	output->avail_out = (uint32)input->avail_out;
	output->total_out = (uint32)input->total_out;

	output->msg = input->msg;
	output->state = (void*)input->state;

	output->data_type = input->data_type;
	output->adler = input->adler;
	output->reserved = input->reserved;

}

void zlib125Export_inflateInit_(PPCInterpreter_t* hCPU)
{
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
	ppcDefineParamStr(version, 1);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs, false);

	// setup internal memory allocator if requested
	if (zstream->zalloc == nullptr)
		zstream->zalloc = PPCInterpreter_makeCallableExportDepr(zlib125_zcalloc);
	if (zstream->zfree == nullptr)
		zstream->zfree = PPCInterpreter_makeCallableExportDepr(zlib125_zcfree);

	sint32 r = inflateInit_(&hzs, version, sizeof(z_stream));

	zlib125_setupUpdateZStream(&hzs, zstream);

	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_inflateInit2_(PPCInterpreter_t* hCPU)
{
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
	ppcDefineParamS32(windowBits, 1);
	ppcDefineParamStr(version, 2);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs, false);

	// setup internal memory allocator if requested
	if (zstream->zalloc == nullptr)
		zstream->zalloc = PPCInterpreter_makeCallableExportDepr(zlib125_zcalloc);
	if (zstream->zfree == nullptr)
		zstream->zfree = PPCInterpreter_makeCallableExportDepr(zlib125_zcfree);

	sint32 r = inflateInit2_(&hzs, windowBits, version, sizeof(z_stream));

	zlib125_setupUpdateZStream(&hzs, zstream);

	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_inflate(PPCInterpreter_t* hCPU)
{
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
	ppcDefineParamS32(flushParam, 1);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs);

	sint32 r = inflate(&hzs, flushParam);

	zlib125_setupUpdateZStream(&hzs, zstream);
	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_inflateEnd(PPCInterpreter_t* hCPU)
{
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs);

	sint32 r = inflateEnd(&hzs);

	zlib125_setupUpdateZStream(&hzs, zstream);
	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_inflateReset(PPCInterpreter_t* hCPU)
{
	debug_printf("inflateReset(0x%08x)\n", hCPU->gpr[3]);
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs);

	sint32 r = inflateReset(&hzs);

	zlib125_setupUpdateZStream(&hzs, zstream);
	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_inflateReset2(PPCInterpreter_t* hCPU)
{
	debug_printf("inflateReset2(0x%08x,0x%08x)\n", hCPU->gpr[3], hCPU->gpr[4]);
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
	ppcDefineParamS32(windowBits, 1);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs);

	sint32 r = inflateReset2(&hzs, windowBits);

	zlib125_setupUpdateZStream(&hzs, zstream);
	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_deflateInit2_(PPCInterpreter_t* hCPU)
{
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
	ppcDefineParamS32(level, 1);
	ppcDefineParamS32(method, 2);
	ppcDefineParamS32(windowBits, 3);
	ppcDefineParamS32(memLevel, 4);
	ppcDefineParamS32(strategy, 5);
	ppcDefineParamStr(version, 6);
	ppcDefineParamS32(streamsize, 7);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs, false);

	// setup internal memory allocator if requested
	if (zstream->zalloc == nullptr)
		zstream->zalloc = PPCInterpreter_makeCallableExportDepr(zlib125_zcalloc);
	if (zstream->zfree == nullptr)
		zstream->zfree = PPCInterpreter_makeCallableExportDepr(zlib125_zcfree);

	// workaround for Splatoon (it allocates a too small buffer for our version of zLib and its zalloc returns NULL)
	zstream->zalloc = PPCInterpreter_makeCallableExportDepr(zlib125_zcalloc);
	zstream->zfree = PPCInterpreter_makeCallableExportDepr(zlib125_zcfree);

	if (streamsize != sizeof(z_stream_ppc2))
		assert_dbg();

	sint32 r = deflateInit2_(&hzs, level, method, windowBits, memLevel, strategy, version, sizeof(z_stream));

	zlib125_setupUpdateZStream(&hzs, zstream);

	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_deflateBound(PPCInterpreter_t* hCPU)
{
	debug_printf("deflateBound(0x%08x,0x%08x)\n", hCPU->gpr[3], hCPU->gpr[4]);
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
	ppcDefineParamS32(sourceLen, 1);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs);

	sint32 r = deflateBound(&hzs, sourceLen);

	zlib125_setupUpdateZStream(&hzs, zstream);
	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_deflate(PPCInterpreter_t* hCPU)
{
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
	ppcDefineParamS32(flushParam, 1);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs);

	sint32 r = deflate(&hzs, flushParam);

	zlib125_setupUpdateZStream(&hzs, zstream);
	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_deflateEnd(PPCInterpreter_t* hCPU)
{
	ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);

	z_stream hzs;
	zlib125_setupHostZStream(zstream, &hzs);

	sint32 r = deflateEnd(&hzs);

	zlib125_setupUpdateZStream(&hzs, zstream);
	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_uncompress(PPCInterpreter_t* hCPU)
{
	// Bytef * dest, uLongf * destLen, const Bytef * source, uLong sourceLen
	uint8* memDst = memory_getPointerFromVirtualOffset(hCPU->gpr[3]);
	uint8* memSrc = memory_getPointerFromVirtualOffset(hCPU->gpr[5]);
	uint32* pDestLenBE = (uint32*)memory_getPointerFromVirtualOffset(hCPU->gpr[4]);
	uint32 sourceLen = hCPU->gpr[6];
	uLong destLen = _swapEndianU32(*pDestLenBE);
	sint32 r = uncompress(memDst, &destLen, memSrc, sourceLen);
	*pDestLenBE = _swapEndianU32(destLen);

	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_compress(PPCInterpreter_t* hCPU)
{
	// Bytef* dest, uLongf* destLen, const Bytef* source, uLong sourceLen
	uint8* memDst = memory_getPointerFromVirtualOffset(hCPU->gpr[3]);
	uint8* memSrc = memory_getPointerFromVirtualOffset(hCPU->gpr[5]);
	uint32* pDestLenBE = (uint32*)memory_getPointerFromVirtualOffset(hCPU->gpr[4]);
	uint32 sourceLen = hCPU->gpr[6];
	uLong destLen = _swapEndianU32(*pDestLenBE);
	sint32 r = compress(memDst, &destLen, memSrc, sourceLen);
	*pDestLenBE = _swapEndianU32(destLen);
	osLib_returnFromFunction(hCPU, r);
}

void zlib125Export_crc32(PPCInterpreter_t* hCPU)
{
	uint32 crc = hCPU->gpr[3];
	uint8* buf = (uint8*)memory_getPointerFromVirtualOffsetAllowNull(hCPU->gpr[4]);
	uint32 len = hCPU->gpr[5];

	uint32 crcResult = crc32(crc, buf, len);

	osLib_returnFromFunction(hCPU, crcResult);
}

void zlib125Export_compressBound(PPCInterpreter_t* hCPU)
{
	uint32 result = compressBound(hCPU->gpr[3]);
	osLib_returnFromFunction(hCPU, result);
}

namespace zlib
{
	void load()
	{
		// zlib125
		osLib_addFunction("zlib125", "inflateInit2_", zlib125Export_inflateInit2_);
		osLib_addFunction("zlib125", "inflateInit_", zlib125Export_inflateInit_);
		osLib_addFunction("zlib125", "inflateEnd", zlib125Export_inflateEnd);
		osLib_addFunction("zlib125", "inflate", zlib125Export_inflate);
		osLib_addFunction("zlib125", "inflateReset", zlib125Export_inflateReset);
		osLib_addFunction("zlib125", "inflateReset2", zlib125Export_inflateReset2);

		osLib_addFunction("zlib125", "deflateInit2_", zlib125Export_deflateInit2_);
		osLib_addFunction("zlib125", "deflateBound", zlib125Export_deflateBound);
		osLib_addFunction("zlib125", "deflate", zlib125Export_deflate);
		osLib_addFunction("zlib125", "deflateEnd", zlib125Export_deflateEnd);

		osLib_addFunction("zlib125", "uncompress", zlib125Export_uncompress);
		osLib_addFunction("zlib125", "compress", zlib125Export_compress);

		osLib_addFunction("zlib125", "crc32", zlib125Export_crc32);
		osLib_addFunction("zlib125", "compressBound", zlib125Export_compressBound);
	}
}