#include "main.h"

#pragma warning(disable:4200)

extern BYTE ff7loopstart[12];
extern BYTE ff7loopend[10];

extern cfg_int cfg_hack_xg_drums, cfg_hack_dls_instruments, cfg_hack_dls_drums, cfg_ff7loopz;

typedef union
{
	BYTE b[4];
	DWORD dw;
} b_dw;

typedef struct
{
	DWORD pos, tm, sz;
	BYTE le;
	BYTE data[];
}
TRACK;

DWORD _fastcall rev32(DWORD);
//WORD _fastcall rev16(WORD);



int test_drum_kit(DWORD no, IDirectMusicCollection* dls);
void do_dls_check(DWORD * i, IDirectMusicCollection * dls);


class CCleaner
{
public:
	INSTRUMENT_DESC* instr, **instr_ptr;
	BYTE ctab[16][128];
	//	UINT dm_vol;
	grow_buf outbuf;
	UINT ntrax, ntrax1;
	UINT maxvol;
	TRACK** in_trax;
	TRACK* out_trax[16];
	DWORD ct;
	UINT tf;
	MIDI_file* mf;
	DWORD vol_set;

	bool drumfix, insfix;
	b_dw ins[16], ins_set[16];

	bool f2, tr1, dm, only_ins, ins_no_lsb;
	bool hasnotes[16];
	void check_ins(UINT msb, UINT lsb, UINT patch, UINT note, BOOL drum, UINT ch) //called on note
	{
		if (ins_no_lsb) lsb = 0;
		INSTRUMENT_DESC * d = instr;
		while (d)
		{
			if (d->bank_hi == msb && d->bank_lo == lsb && d->patch == patch && d->drum == drum) break;
			d = d->next;
		}
		if (d)
		{
			d->count++;
			if (d->note_max < note) d->note_max = note;
			if (d->note_min > note) d->note_min = note;
			d->channels |= 1 << ch;
		}
		else
		{
			d = new INSTRUMENT_DESC;
			*instr_ptr = d;
			instr_ptr = &d->next;
			d->next = 0;
			d->note_min = d->note_max = note;
			d->bank_hi = msb;
			d->bank_lo = lsb;
			d->patch = patch;
			d->count = 1;
			d->drum = drum;
			d->user = 0;
			d->channels = 1 << ch;
		}
	}
	void AdvanceTime(TRACK* t);
	void AddEvent(BYTE ev, BYTE* data);
	void WriteTrack(TRACK* t);
	int Run(MIDI_file* mf, DWORD, void ** out_data, int * out_size);

	void do_shit(UINT n);

	UINT get_next_time()
	{
		UINT t = -1;
		UINT n;
		for (n = 0;n < ntrax;n++)
		{
			UINT t1 = in_trax[n]->tm;
			if (t1 < t) t = t1;
		}
		return t;
	}

	BOOL event_ok(BYTE e, BYTE* p)
	{
		BYTE _c = e & 0xF0;
		BYTE ch = e & 0xF;
		if (_c == 0xB0)
		{
			if (cfg_hack_xg_drums && ch == 9 && p[1] == 0 && (p[0] == 0 || p[0] == 0x20)) 
				return 0;

			if (p[0] > 127)
				return 0;

			ctab[ch][p[0]] = p[1];


			if (p[0] == 0)
			{
				ins[ch].b[2] = p[1];
				if (insfix) return 0;
			}
			if (p[0] == 0x20)
			{
				ins[ch].b[1] = p[1];
				if (insfix) return 0;
			}

			if (dm)	//keep dm drivers happy...
			{
				if (p[0] >= 0x20 && p[0] < 0x40) //lsb values
				{
					return 0;
				}
				else if (p[0] < 0x20)
				{
					BYTE data[2] = {(BYTE)(p[0] + 0x20),ctab[ch][p[0] + 0x20]};
					AddEvent(e, data);
				}
			}

			return 1;

		}

		else if (_c == 0xC0)
		{
			if (ch == 9)
			{
				if (drumfix && !test_drum_kit(p[0], mf->pDLS)) return 0;
				ins[ch].b[0] = p[0];
			}
			else
			{
				ins[ch].b[0] = p[0];
				if (insfix) return 0;
			}
		}
		else if (_c == 0x90 && p[1])
		{
			if (only_ins)
				check_ins(ins[ch].b[2], ins[ch].b[1], ins[ch].b[0], p[0], ch == 9, ch);
			if (ch != 9 && insfix)
			{
				if (ins_set[ch].dw != ins[ch].dw)
				{
					do_dls_check(&ins[ch].dw, mf->pDLS);


					if (ins_set[ch].b[1] != ins[ch].b[1])
					{
						BYTE t[2] = {0x20, ins[ch].b[1]};
						AddEvent(0xB0 | ch, t);
					}

					if (ins_set[ch].b[2] != ins[ch].b[2])
					{
						BYTE t[2] = {0, ins[ch].b[2]};
						AddEvent(0xB0 | ch, t);
					}
					AddEvent(0xC0 | ch, ins[ch].b);

					ins_set[ch].dw = ins[ch].dw;
				}
			}
		}

		return 1;
	}

	CCleaner()
	{
		memset(ins, 0, sizeof(ins));
		memset(ins_set, -1, sizeof(ins_set));
		memset(hasnotes, 0, sizeof(hasnotes));
		memset(out_trax, 0, sizeof(out_trax));
		in_trax = 0;
	}
	~CCleaner()
	{
		UINT n;
		if (in_trax)
		{
			for (n = 0;n < ntrax;n++)
			if (in_trax[n]) {free(in_trax[n]);in_trax[n] = 0;}
			free(in_trax);
		}
		for (n = 0;n < 16;n++)
		{
			if (out_trax[n])
			{
				free(out_trax[n]);
				out_trax[n] = 0;
			}
		}
	}
};

void CCleaner::do_shit(UINT n)
{
	BYTE ce = 0;
	TRACK* t = in_trax[n];
	if (!t) return ;
	while (t->tm == ct)
	{

		if (t->pos >= t->sz)
		{
			t->pos = -1;
			t->tm = -1;
			tf++;
			break;
		}
		BYTE c0 = t->data[t->pos];
		if (c0 == 0xFF) //Meta-events
		{

			if (cfg_ff7loopz
			        && (t->sz - t->pos) >= sizeof(ff7loopend) // bounds check
			        && !memcmp(t->data + t->pos, ff7loopend, sizeof(ff7loopend)))
			{
				//				MessageBox(GetActiveWindow(),"blah",0,0);
				//				AdvanceTime(t);
				tf = ntrax;
				//				return;
			}
			BYTE c1 = t->data[t->pos + 1];
			if (c1 == 0x2F)
			{
				t->pos += 3;
				t->tm = -1;
				tf++;
			}
			{
				t->pos += 2;
				if (t->pos < t->sz)
				{

					unsigned int _d;
					t->pos += DecodeDelta(t->data + t->pos, &_d, t->sz - t->pos);
					t->pos += _d;
				}
			}
		} else if (c0 == 0xF0)
		{
			t->pos += ReadSysex(&t->data[t->pos], t->sz - t->pos);
		}
		else if (c0 == 0xF7) t->pos++;
		else if ((c0&0xF0) == 0xF0) //WTF?
		{
			t->pos = -1;
			t->tm = -1;
			tf++;
			break;
		}
		else
		{
			if (c0&0x80)
			{
				ce = c0;
				t->pos++;
			}
			else ce = t->le;

			if (event_ok(ce, &t->data[t->pos])) AddEvent(ce, &t->data[t->pos]);

			if ((ce&0xF0) == 0xC0 || (ce&0xF0) == 0xD0) t->pos++;
			else t->pos += 2;
			t->le = ce;
		}

		if (t->tm != -1 && t->pos >= t->sz)
		{
			t->pos = -1;
			t->tm = -1;
			tf++;
			break;
		}
		AdvanceTime(t);
	}
}

#define WriteBuf(A,B) outbuf.write(A,B)

#pragma pack(push)
#pragma pack(1)
typedef struct
{
	WORD t, n, d;
}
MHD;
typedef struct
{
	DWORD c, s;
}
CHD;
#pragma pack(pop)


void CCleaner::AdvanceTime(TRACK* t)
{
	if (t->tm != -1)
	{
		unsigned int d;
		UINT _n = DecodeDelta(t->data + t->pos, &d, t->sz - t->pos);
		if (_n < 4) t->tm += d;
		t->pos += _n;
	}
}

void CCleaner::AddEvent(BYTE ev, BYTE* data)
{
	if (only_ins) return ;
	BYTE nt = ev & 0xF;
	BYTE ec = ev & 0xF0;
	if (tr1) nt = 0;
	TRACK *t = out_trax[nt];

	ZeroMemory(ctab, sizeof(ctab));


	if (!t)
	{
		t = out_trax[nt] = (TRACK*)malloc(sizeof(TRACK) + 0x1000);
		if (!t) return ;
		ZeroMemory(t, 16);
		t->sz = 0x1000;
		t->tm = 0;

	}
	else if (t->pos > t->sz - 0x10)
	{
		t->sz *= 2;
		out_trax[nt] = (TRACK*)realloc(t, sizeof(TRACK) + t->sz);
		if (!out_trax[nt])
		{
			free(t);
			return ;
		}
		t = out_trax[nt];
	}

	if (t->tm < ct)
	{
		t->pos += EncodeDelta(&t->data[t->pos], ct - t->tm);
		t->tm = ct;
	}
	else
	{
		t->data[t->pos++] = 0;
	}
	if (ec == 0x90)
	{
		hasnotes[nt] = 1;
		data[0] &= 0x7F; /* don't allow 8bit note numbers */
	}
	else if (ec == 0x80)
	{
		data[0] &= 0x7F; /* don't allow 8bit note numbers */
	}
	/*if (ev!=t->le) */{t->data[t->pos++] = ev;t->le = ev;}
	t->data[t->pos++] = data[0];
	if (ec != 0xC0 && ec != 0xD0) t->data[t->pos++] = data[1];
}

void CCleaner::WriteTrack(TRACK* t)
{
	CHD chd;
	chd.c = 'krTM';
	chd.s = rev32(t->pos);
	WriteBuf(&chd, 8);
	WriteBuf(&t->data, t->pos);
	ntrax1++;
}

int DoCleanUp(MIDI_file* mf, DWORD mode, void** out_data, int * out_size)
{
	CCleaner c;
	c.only_ins = 0;
	return c.Run(mf, mode, out_data, out_size);
}

int CCleaner::Run(MIDI_file* _mf, DWORD _md, void ** out_data, int * out_size)
{
	f2 = *(WORD*)(_mf->data + 8) == 0x0200;
	maxvol = 90;
	vol_set = 0;
	dm = (_md & CLEAN_DM) ? 1 : 0;
	tr1 = (_md & CLEAN_1TRACK) ? 1 : 0;

	if (_md&CLEAN_DLS)
	{
		drumfix = dm && cfg_hack_dls_drums;
		insfix = dm && cfg_hack_dls_instruments;
	}
	else
	{
		drumfix = insfix = 0;
	}



	mf = _mf;

	instr_ptr = &instr;
	instr = 0;

	UINT n;

	ct = 0;
	tf = 0;
	ntrax = ntrax1 = 0;
	CHD chd;
	MHD mhd;
	DWORD ptr = 8;



	mhd = *(MHD*)(mf->data + 8);

	ptr += 6;

	mhd.t = rev16(mhd.t);
	mhd.n = rev16(mhd.n);

	if (mhd.t > 2)
		goto fail;
	ntrax = mhd.n;
	n = 0;
	in_trax = (TRACK**)malloc(sizeof(void*) * ntrax);
	for (;n < ntrax && ptr < (UINT)mf->size;n++)
	{
		chd = *(CHD*)(mf->data + ptr);
		ptr += 8;
		if (chd.c != 'krTM' || ptr > (UINT)mf->size)
		{
			ntrax = n;
			break;
		}
		chd.s = rev32(chd.s);
		//if (ptr+chd.s>(UINT)mf->size)
		if (chd.s > ((UINT)mf->size - ptr))
		{
			chd.s = mf->size - ptr;
		}
		//goto fail;
		in_trax[n] = (TRACK*)malloc(16 + chd.s);
		in_trax[n]->sz = chd.s;
		in_trax[n]->tm = 0;
		in_trax[n]->le = 0;
		in_trax[n]->pos = 0;
		memcpy(in_trax[n]->data, mf->data + ptr, chd.s);
		ptr += chd.s;
		AdvanceTime(in_trax[n]);
	}
	if (f2)
	{
		for (n = 0;n < ntrax;n++)
		{
			in_trax[n]->tm = ct;
			while (tf <= n)
			{
				do_shit(n);
				if (in_trax[n]->tm != -1) ct = in_trax[n]->tm;
			}
		}
	}
	else
	{
		while (tf < ntrax)
		{
			UINT nt = get_next_time(); //ct++;
			if (nt == -1) break;
			ct = nt;
			for (n = 0;n < ntrax && tf < ntrax;n++)
			{
				do_shit(n);
			}
		}
	}

	if (!only_ins)
	{


		mhd.t = 0x0100;
		mhd.n = 0; //rev16(ntrax1);
		chd.c = 'dhTM';
		chd.s = 0x06000000;
		WriteBuf(&chd, 8);
		WriteBuf(&mhd, 6);
		if (!(_md&CLEAN_NOTEMPO) && mf->tmap)
		{
			/*			BYTE *tt=mf->tmap->BuildTrack();
						if (tt)
						{
							WriteBuf(tt,rev32(*(DWORD*)(tt+4))+8);
							ntrax1++;
							free(tt);
						}*/
			if (mf->tmap->BuildTrack(outbuf))
			{
				ntrax1++;
			}
		}
		if (!(_md&CLEAN_NOSYSEX) && mf->smap)
		{
			/*			BYTE *st=mf->smap->BuildTrack();
						if (st)
						{
							WriteBuf(st,rev32(*(DWORD*)(st+4))+8);
							ntrax1++;
							free(st);
						}*/
			if (mf->smap->BuildTrack(outbuf))
			{
				ntrax1++;
			}
		}



		for (n = 0;n < 16;n++) if (out_trax[n] && hasnotes[n] && out_trax[n]->pos)
			{
				TRACK *t = out_trax[n];
				t->pos += EncodeDelta(t->data + t->pos, ct - t->tm);
				t->data[t->pos++] = 0xFF;
				t->data[t->pos++] = 0x2F;
				t->data[t->pos++] = 0;
				WriteTrack(t);
			}
		{
			WORD t = rev16(ntrax1);
			outbuf.write_ptr(&t, 2, 10);
		}
		if (out_size) *out_size = outbuf.get_size();
		if (out_data) *out_data = outbuf.finish();
#if 0
		{
			HANDLE f = CreateFile("c:\\dump.mid", GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0);
			DWORD bw = 0;
			WriteFile(f, rv, bs, &bw, 0);
			CloseHandle(f);
		}
#endif

	}
	return 1;
fail:
	//	ErrorBox("WARNING: cleaner messed up");

	return 0;

	//TO DESTRUCTOR

}

INSTRUMENT_DESC* GetInstruments(MIDI_file* mf, BOOL do_lsb)
{
	CCleaner c;
	c.only_ins = 1;
	c.ins_no_lsb = !do_lsb;
	c.Run(mf, 0, 0, 0);
	return c.instr;
}