#include "device/fmgen/headers.h"
#include "device/fmgen/opna.h"
#include "kmp_pi.h"

#include "device/s_vrc7.h"
#include "device/s_sng.h"

#define USE_ZLIB 1
#define USE_BZLIB (1 && USE_ZLIB)


#define KMPMODULE_REENTRANT 0
#define KMPMODULE_PLUGIN_VERSION    10
#define KMPMODULE_PLUGIN_NAME       "m_gym.kpi v1.0r10 by Mamiya"
#define FMGEN_COPYRIGHT "FM Sound Generator Copyright (C) by cisc 1998, 2003."
#define ZLIB_COPYRIGHT "zlib 1.1.4 (C) 1995-1998 Jean-loup Gailly and Mark Adler"
#define BZLIB_COPYRIGHT "bzlib 1.0.2 copyright (C) 1996-2002 Julian R Seward."
#if USE_ZLIB && USE_BZLIB
#define KMPMODULE_PLUGIN_COPYRIGHT FMGEN_COPYRIGHT " / " ZLIB_COPYRIGHT " / " BZLIB_COPYRIGHT
#elif USE_ZLIB
#define KMPMODULE_PLUGIN_COPYRIGHT FMGEN_COPYRIGHT " / " ZLIB_COPYRIGHT
#elif USE_BZLIB
#define KMPMODULE_PLUGIN_COPYRIGHT FMGEN_COPYRIGHT " / " BZLIB_COPYRIGHT
#else
#define KMPMODULE_PLUGIN_COPYRIGHT FMGEN_COPYRIGHT
#endif

#if USE_ZLIB
#include <zlib.h>
#endif
#if USE_BZLIB
#include "bzlib/bzlib.h"
#endif

#define MASTER_CLOCK (53693175)
#define MAX_SAMPLE_RATE 96000
#define SYNC_RATE 60 /* (Hz) */
#define MAX_UNIT_RENDER (MAX_SAMPLE_RATE/SYNC_RATE)
#define FADEOUT_TIME (10 * SYNC_RATE)

/* GYM(ymamp) file header struct */
#define GYM_MAGIC (0x584d5947)	/* 'GYMX' */
#define GYM_MAGIC_EZPK (0x4b505a45)	/* 'EZPK' */
#define GYM_OFS_MAGIC		(0)
#define GYM_OFS_SONGTITLE	(GYM_OFS_MAGIC +      4)
#define GYM_OFS_GAMETITLE	(GYM_OFS_SONGTITLE + 32)
#define GYM_OFS_PUBLISHER	(GYM_OFS_GAMETITLE + 32)
#define GYM_OFS_GENERATOR	(GYM_OFS_PUBLISHER + 32)
#define GYM_OFS_DUMPER		(GYM_OFS_GENERATOR + 32)
#define GYM_OFS_COMMENTS	(GYM_OFS_DUMPER +    32)
#define GYM_OFS_LOOPTIME	(GYM_OFS_COMMENTS + 256)
#define GYM_OFS_COMPSIZE	(GYM_OFS_LOOPTIME +   4)
#define GYM_OFS_DATASTART	(GYM_OFS_COMPSIZE +   4)
#define GYM_OFS_MAGICCOMP	(GYM_OFS_COMPSIZE +   4)
#define GYM_OFS_COMPSIZE2	(GYM_OFS_MAGICCOMP +   4)
#define GYM_OFS_DATASTART2	(GYM_OFS_COMPSIZE2 +   4)


class gymFile {
public:
	bool OpenFromBuffer(const BYTE *Buffer, DWORD dwSize, SOUNDINFO *pInfo);
	/* bool Open(const char *cszFileName, SOUNDINFO *pInfo);*/
	void Close(void);
	DWORD SetPosition(DWORD dwpos);
	DWORD Write(Int16 *Buffer, DWORD numSample);
	gymFile();
	~gymFile();
protected:
	FM::OPN2 *opn2;
	bool fopn2;
	OPLLSOUND *opll;
	bool fopll;
	SNGSOUND *sng;
	bool fsng;

	bool fopn2_dac_not_initialized;
	unsigned fopn2_dac_count;

	BYTE *gymdata;
	BYTE *gymtop;
	BYTE *gymloop;
	int length;
	DWORD playtime; /* syncs */
	DWORD looptime; /* syncs */

	BYTE *gymcur;
	DWORD curtime;

	DWORD fader;
	int loopnum;
	int loopcur;

	unsigned srate;
	unsigned unit_render;

	DWORD lefthi;
	DWORD leftlo;
	DWORD stephi;
	DWORD steplo;
	Int16 StepBuffer[(MAX_UNIT_RENDER + 1) * 2];
	Int16 bufsn[(MAX_UNIT_RENDER + 1) * 2];
	FM::Sample bufopn2[(MAX_UNIT_RENDER + 1) * 2];

	void CalcTime(void);
	bool Step(void);
	void Reset(void);

	bool FillBuffer();
};

static int GetDwordLE(unsigned char *p)
{
	int ret;
	ret  = ((int)(unsigned char)p[3]) << 0x18;
	ret |= ((int)(unsigned char)p[2]) << 0x10;
	ret |= ((int)(unsigned char)p[1]) << 0x08;
	ret |= ((int)(unsigned char)p[0]) << 0x00;
	return ret;
}

static void SetDwordLE(unsigned char *p, unsigned long v)
{
	p[0] = (unsigned char)(v >> (8 * 0));
	p[1] = (unsigned char)(v >> (8 * 1));
	p[2] = (unsigned char)(v >> (8 * 2));
	p[3] = (unsigned char)(v >> (8 * 3));
}

gymFile::gymFile()
{
	gymdata = 0;
	opn2 = 0;
	sng = 0;
	opll = 0;
}
gymFile::~gymFile()
{
	Close();
}

void gymFile::CalcTime(void)
{
	BYTE *p = gymtop;
	gymloop = 0;
	playtime = 0;
	fopn2_dac_not_initialized = true;
	fopn2_dac_count = 0;
	if (!gymdata) return;
	while (1)
	{
		switch (*p)
		{
			case 0x00:	/* SYNC */
				playtime++;
				p += 1;
				if (playtime == looptime) gymloop = p;
				break;
			case 0x01:	/* OPN2(M) */
				p += 3;
				if (p[1] == 0x2b)
				{
					if (p[2] & 0x80) fopn2_dac_not_initialized = false;
				}
				else if (p[1] == 0x2a)
				{
					fopn2_dac_count++;
				}
				break;
			case 0x02:	/* OPN2(S) */
				p += 3;
				break;
			case 0x03:	/* SNG */
			case 0x04:	/* GG-STEREO */
			case 0x05:	/* OPLL(a) */
			case 0x06:	/* OPLL(d) */
				p += 2;
				break;
			case 0xff:	/* gurdian */
				return;
			default:
				p+=1;
				break;
		}
	}
}

static Uint32 DivFix(Uint32 p1, Uint32 p2, Uint32 fix)
{
	Uint32 ret;
	ret = p1 / p2;
	p1  = p1 % p2;/* p1 = p1 - p2 * ret; */
	while (fix--)
	{
		p1 += p1;
		ret += ret;
		if (p1 >= p2)
		{
			p1 -= p2;
			ret++;
		}
	}
	return ret;
}

void gymFile::Reset(void)
{
	opn2->Reset();
	opn2->SetReg(0x29, 0x9f);
	opn2->SetReg(0x2a, 0x80);
	opn2->SetReg(0x2b, 0x00);
	opll->Reset();
	sng->Reset();

	if (fopn2_dac_not_initialized && fopn2_dac_count > 255)
		opn2->SetReg(0x2b, 0x080);

	fader = 0;
	loopcur = 0;

	gymcur = gymtop;
	curtime = 0;
	fopn2 = fopll = fsng = false;

	steplo = DivFix((srate % 60), 60, 31);
	leftlo = 0;
	FillBuffer();

}

bool gymFile::Step(void)
{
	if (fader && ++fader >= FADEOUT_TIME) return true;
	while (*gymcur)
	{
		switch (*gymcur)
		{
			case 0x01:	/* OPN2(M) */
				fopn2 = true;
				opn2->SetReg(gymcur[1], gymcur[2]);
				gymcur += 3;
				break;
			case 0x02:	/* OPN2(S) */
				fopn2 = true;
				opn2->SetReg(gymcur[1] | 0x100, gymcur[2]);
				gymcur += 3;
				break;
			case 0x03:	/* SNG */
				fsng = true;
				sng->WriteData(gymcur[1]);
				gymcur += 2;
				break;
			case 0x04:	/* GG-STEREO */
				fsng = true;
				sng->WriteStereo(gymcur[1]);
				gymcur += 2;
				break;
			case 0x05:	/* OPLL(a) */
				opll->WriteAddress(gymcur[1]);
				gymcur += 2;
				break;
			case 0x06:	/* OPLL(d) */
				fopll = true;
				opll->WriteData(gymcur[1]);
				gymcur += 2;
				break;
			case 0xff:	/* gurdian */
				if (gymloop)
				{
					gymcur = gymloop;
					if (loopnum && !fader &&  ++loopcur == loopnum) fader = 1;
					break;
				}
				fopn2 = fopll = fsng = false;
				return true;
			default:
				gymcur += 1;
				break;
/*
				fopn2 = fopll = fsng = false;
				return true;
*/
		}
	}
	gymcur++;
	curtime++;
	return false;
}

DWORD gymFile::SetPosition(DWORD dwpos)
{
	if (!gymdata) return 0;
	if (dwpos < curtime) Reset();
	while (dwpos > curtime && !Step());
	leftlo = 0;
	FillBuffer();
	return curtime;
}

#if USE_BZLIB
unsigned long decomp_dac(unsigned char *srcp, unsigned long srcl, unsigned char *desp, unsigned long desl)
{
	if (GetDwordLE(srcp + 0) != GYM_MAGIC_EZPK) return 0;
	unsigned long ul = GetDwordLE(srcp + 4);
	if (desl == 0) return ul & 0xfffffff;
	if ((ul & 0xf0000000) == 0)
	{
		unsigned int ui = desl;
		if (BZ2_bzBuffToBuffDecompress((char *)desp, &ui, (char *)(srcp + 8), srcl, 0, 0) < 0)
			return 0;
		return desl;
	}
	else
	{
		z_stream zs;
		memset(&zs, 0, sizeof(z_stream));
		zs.next_in = (Bytef *)(srcp + 8);
		zs.avail_in = srcl - 8;
		zs.next_out = (Bytef *)desp;
		zs.avail_out = desl;
		if ((inflateInit(&zs)) < 0) return 0;
		if ((inflate(&zs, Z_FINISH)) >= 0)
			ul = zs.total_out;
		else
			ul = 0;
		inflateEnd(&zs);
		return ul;
	}
}
#endif

bool gymFile::OpenFromBuffer(const BYTE *Buffer, DWORD dwSize, SOUNDINFO *pInfo)
{
	Close();
	srate = (pInfo->dwSamplesPerSec == 0) ? 44100 : pInfo->dwSamplesPerSec;
	if (srate > MAX_SAMPLE_RATE) srate = MAX_SAMPLE_RATE;
	unit_render = srate / SYNC_RATE;
	opn2 = new FM::OPN2();
	opll = new OPLLSOUND(srate,MASTER_CLOCK / 15);
	sng  = new SNGSOUND(srate,MASTER_CLOCK / 15);
	if (!opn2 || !opll || !sng) return false;
	opn2->Init(MASTER_CLOCK / 7, srate, true);

	BYTE *buf = 0;
	DWORD length = dwSize;
	do
	{
		if (length < 4) break;
		buf = new BYTE[length + 4];
		if (!buf) break;
		XMEMCPY(buf, Buffer, length);
		if (GetDwordLE(buf + GYM_OFS_MAGIC) == GYM_MAGIC)
		{
			looptime = GetDwordLE(buf + GYM_OFS_LOOPTIME);
			if (GetDwordLE(buf + GYM_OFS_COMPSIZE))
			{
				if (GetDwordLE(buf + GYM_OFS_MAGICCOMP) == GYM_MAGIC_EZPK)
				{
#if USE_BZLIB
					unsigned int msize = GetDwordLE(buf + GYM_OFS_COMPSIZE) + 0x24;
					unsigned char *mbuf = new unsigned char [msize];
					if (!mbuf) break;
					int berr = BZ2_bzBuffToBuffDecompress((char *)mbuf, &msize, (char *)(buf + GYM_OFS_DATASTART2), GetDwordLE(buf + GYM_OFS_COMPSIZE2)-8, 0, 0);
					if (berr < 0 || msize < 0x24)
					{
						delete [] mbuf;
						break;
					}
					length = /*msize - 0x24*/GetDwordLE(buf + GYM_OFS_COMPSIZE);
					unsigned char *obuf = new unsigned char [length + 4];
					if (!obuf)
					{
						delete [] mbuf;
						break;
					}
					unsigned long xsize = GetDwordLE(mbuf + 0x18) + GetDwordLE(mbuf + 0x1c);
					if (xsize >= 100)
					{
						unsigned char *xbuf = new unsigned char [xsize];
						if (!xbuf)
						{
							delete [] mbuf;
							break;
						}
						unsigned long ret = decomp_dac(mbuf + GetDwordLE(mbuf + 0x20),GetDwordLE(mbuf + 0x14),xbuf,xsize);
						if (ret) memcpy(mbuf + GetDwordLE(mbuf + 0x20), xbuf, ret);
						delete [] xbuf;
					}
					unsigned char *typep, *sngp, *regp, *datmp, *datsp, *dacmp, *dacsp;
					{
						unsigned char *p1;
						p1 = mbuf + 0x24;
						typep = p1; p1 += GetDwordLE(mbuf + 0x00);
						regp  = p1; p1 += GetDwordLE(mbuf + 0x04);
						datmp = p1; p1 += GetDwordLE(mbuf + 0x08);
						datsp = p1; p1 += GetDwordLE(mbuf + 0x0c);
						sngp  = p1; p1 += GetDwordLE(mbuf + 0x10);
						dacmp = p1; p1 += GetDwordLE(mbuf + 0x18);
						dacsp = p1;
					}
					unsigned char *op = obuf, *pe = regp;
					do {
						unsigned char uc;
						*(op++) = uc = *typep;
						switch (uc)
						{
							case 1:
								*(op++) = uc = *(regp++);
								if (uc == 0x2a)
									*(op++) = *(dacmp++);
								else
									*(op++) = *(datmp++);
								break;
							case 2:
								*(op++) = uc = *(regp++);
								if (uc == 0x2a)
									*(op++) = *(dacsp++);
								else
									*(op++) = *(datsp++);
								break;
							case 3:
								*(op++) = *(sngp++);
								break;
							case 0:
								break;
							default:
								break;
						}
						typep++;
					} while (op < obuf + length/*++typep < pe*/);
					length = op - obuf;
					gymdata = gymtop = obuf;
					delete [] mbuf;
#else
					break;	/* NOT SUPPORT */
#endif
				}
				else
				{
#if USE_ZLIB
					uLongf dessize;
					unsigned char *des;
					int zerr;
					des = new unsigned char [GetDwordLE(buf + GYM_OFS_COMPSIZE) + 4];
					if (des == NULL) break;
					zerr = uncompress(des, &dessize, buf + GYM_OFS_DATASTART, length - GYM_OFS_DATASTART);
					if (zerr >= Z_OK)
					{
						length = dessize;
						gymdata = gymtop = des;
					}
					else if (zerr == Z_DATA_ERROR)
					{
						delete [] des;
						gymdata = buf;
						gymtop = buf + GYM_OFS_DATASTART;
						buf = 0;
					}
					else
						break;
#else
					break;	/* NOT SUPPORT */
#endif
				}
			}
			else
			{
				gymdata = buf;
				gymtop = buf + GYM_OFS_DATASTART;
				buf = 0;
			}
		}
		else
		{
			looptime = 0;
			gymdata = gymtop = buf;
			buf = 0;
		}
	} while(0);
	if (buf) delete [] buf;

	if (!gymdata) return false;

	/* gurdian */
	gymdata[length + 0] = 0xff;
	gymdata[length + 1] = 0xff;
	gymdata[length + 2] = 0xff;
	gymdata[length + 3] = 0xff;

	Reset();

	CalcTime();

	pInfo->dwSamplesPerSec = srate;
	pInfo->dwChannels = 2;
	pInfo->dwBitsPerSample = 16;
	pInfo->dwSeekable = 1;
	pInfo->dwUnitRender = unit_render * 4;
	pInfo->dwReserved1 = 1;
	pInfo->dwReserved2 = 0;

	if (looptime)
	{
		loopnum = 2;
		pInfo->dwLength = playtime + (playtime - looptime) * (loopnum - 1) + FADEOUT_TIME;
#if 1
		/* 単曲リピート処理は此処 */
		loopnum = 0;
#endif
	}
	else
	{
		pInfo->dwLength = playtime;
		loopnum = 0;
	}
	/* syncs to msec */
	pInfo->dwLength = pInfo->dwLength * 1000 / SYNC_RATE;

	return true;
}

void gymFile::Close(void)
{
	if (gymdata) { delete [] gymdata; gymdata = 0; }
	if (opn2) { delete opn2; opn2 = 0; }
	if (opll) { delete opll; opll = 0; }
	if (sng) { delete sng; sng = 0; }
}

bool gymFile::FillBuffer()
{
	DWORD i;
	if (Step())
	{
		lefthi = 0;
		return true;
	}
	lefthi = unit_render;
	stephi = 0;
	leftlo += steplo;
	if (leftlo > (1 << 31))
	{
		leftlo -= (1 << 31);
		lefthi++;
	}
	XMEMSET(StepBuffer, 0,  lefthi * sizeof(Int16) * 2);
	if (fopll) opll->Synth(StepBuffer, lefthi);
	if (fsng)
	{
		sng->Synth(bufsn, lefthi);
		for (i = 0; i < lefthi * 2; i++)
		{
			StepBuffer[i] += bufsn[i] >> 1;
		}
	}
	if (fopn2)
	{
		XMEMSET(bufopn2, 0, lefthi * sizeof(FM::Sample) * 2);
		opn2->Mix(bufopn2, lefthi);
		for (i = 0; i < lefthi * 2; i++)
		{
			StepBuffer[i] += bufopn2[i]; 
		}
	}
	if (fader)
	{
		unsigned fadebits = (fader << 4) / FADEOUT_TIME;
		if (fadebits)
		{
			for (i = 0; i < lefthi * 2; i++)
			{
				StepBuffer[i] >>= fadebits;
			}
		}
	}
	return false;
}

DWORD gymFile::Write(Int16 *Buffer, DWORD numSample)
{
	DWORD pos = 0, numWrite = 0;
	if (!gymdata) return numWrite;
	if (numSample == 0) return numWrite;

	while (lefthi && numSample >= lefthi)
	{
		if (Buffer)
		{
			XMEMCPY(&Buffer[pos], &StepBuffer[stephi], lefthi * 4);
			pos += lefthi * 2;
		}
		stephi += lefthi * 2;
		numWrite += lefthi;
		numSample -= lefthi;
		FillBuffer();
	}
	if (lefthi && numSample)
	{
		if (Buffer)
		{
			XMEMCPY(&Buffer[pos], &StepBuffer[stephi], numSample * 4);
			pos += numSample * 2;
		}
		stephi += numSample * 2;
		numWrite += numSample;
		lefthi -= numSample;
	}
	return numWrite;
}

#if !KMPMODULE_REENTRANT
static LONG mutex = 1;
#endif

static HKMP WINAPI kmp_OpenFromBuffer(const BYTE *Buffer, DWORD dwSize, SOUNDINFO *pInfo)
{
	gymFile *pFile = 0;
	LONG signal = 1;
#if !KMPMODULE_REENTRANT
	signal = InterlockedExchange(&mutex,0);
#endif
	while (signal)
	{
		if (!pInfo) break;
		pFile = new gymFile;
		if (!pFile || !pFile->OpenFromBuffer(Buffer, dwSize, pInfo)) break;
		return pFile;
	}
	if (pFile) delete pFile;
#if !KMPMODULE_REENTRANT
	if (signal) InterlockedExchange(&mutex, signal);
#endif
	return 0;
}


static HKMP WINAPI kmp_Open(const char *cszFileName, SOUNDINFO *pInfo)
{
	if (!pInfo) return NULL;
	HKMP ret = 0;
	DWORD dwSize = 0;
	BYTE *Buffer = 0;
	FILE *fp = 0;
	do
	{
		fp = fopen(cszFileName, "rb");
		if (!fp) break;
		fseek(fp, 0, SEEK_END);
		dwSize = ftell(fp);
		if (dwSize < 4) break;
		fseek(fp, 0, SEEK_SET);
		Buffer = new BYTE[dwSize];
		if (!Buffer) break;
		if (dwSize != fread(Buffer, 1, dwSize, fp)) break;
		ret = kmp_OpenFromBuffer(Buffer, dwSize, pInfo);
	} while (0);
	if (Buffer) delete [] Buffer;
	if (fp) fclose(fp);
	return ret;
}

static void WINAPI kmp_Close(HKMP hKMP)
{
	if (hKMP)
	{
		delete ((gymFile*)hKMP);
#if !KMPMODULE_REENTRANT
		InterlockedExchange(&mutex, 1);
#endif
	}
}

static DWORD WINAPI kmp_Render(HKMP hKMP, BYTE* Buffer, DWORD dwSize)
{
	if(!hKMP)return 0;
	DWORD dwWrite;
	dwWrite = ((gymFile*)hKMP)->Write((Int16 *)Buffer, dwSize / 4) * 4;
#if 0
	/* 足りない部分を0にする(必要ない?) */
	if (dwWrite && dwSize > dwWrite)
	{
		if (Buffer) XMEMSET(Buffer + dwWrite, 0, dwSize - dwWrite);
		dwWrite = dwSize;
	}
#endif
	return dwWrite;
}

static DWORD WINAPI kmp_SetPosition(HKMP hKMP, DWORD dwPos)
{
	if(!hKMP)return 0;
	return ((gymFile*)hKMP)->SetPosition(dwPos * SYNC_RATE / 1000) * 1000 / SYNC_RATE;
}

#if 0
static void WINAPI kmp_Init(void) {}
static void WINAPI kmp_Deinit(void) {}
#endif

KMPMODULE* WINAPI kmp_GetTestModule(void)
{
	static const char *ppszSupportExts[] = { ".gym", ".ssl", NULL };
	static KMPMODULE Mod = {
		KMPMODULE_VERSION,
		KMPMODULE_PLUGIN_VERSION,
		KMPMODULE_PLUGIN_COPYRIGHT,
		KMPMODULE_PLUGIN_NAME,
		ppszSupportExts,
		KMPMODULE_REENTRANT,
		NULL,	//Init
		NULL,	//Deinit
		kmp_Open,
		kmp_OpenFromBuffer,	//OpenFromBuffer
		kmp_Close,
		kmp_Render,
		kmp_SetPosition
	};
	return &Mod;
}
