/*
imfplay
Copyright (c) 2014 Tom Grus

This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the
use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it freely,
subject to the following restrictions:

    1. The origin of this software must not be misrepresented; you must not
       claim that you wrote the original software. If you use this software
       in a product, an acknowledgment in the product documentation would be
       appreciated but is not required.

    2. Altered source versions must be plainly marked as such, and must not
       be misrepresented as being the original software.

    3. This notice may not be removed or altered from any source distribution.
*/

#include <stdio.h>
#include <stdlib.h>

#include <conio.h>
#include <dos.h>


#define CHAN_Y	2		//channel data table Y offset
#define C_LOW		8		//low-intensity color
#define C_NORM	7		//normal color
#define C_HI		15	//highlight color
#define C_ACT		10	//active channel color
#define C_NACT	4		//inactive channel color
#define ICL			-1	//invalid channel line

#define XY(x, y) (2 * ((x) + 80 * (y)))

unsigned char *VGA = (unsigned char *)MK_FP(0xB800, 0);
void interrupt (*old_time_handler)(void);
long freq_div;
long last_slow_tick;
const int opl2_base = 0x388;
long time_ctr;
unsigned char shadow_opl[256];
int mutemask[9];

char hex[] = "0123456789ABCDEF";
char dec[16][3] = {" 0", " 1", " 2", " 3", " 4", " 5", " 6", " 7",
	" 8", " 9", "10", "11", "12", "13", "14", "15"};

char logo[] =
"          XX         X\n"
"          X          X\n"
"X         X          X\n"
"          X          X\n"
"X XXXXXXX XX XXXXXXX X XXXXXXX  X     X\n"
"X X  X  X X  X     X X X     X  X     X\n"
"X X  X  X X  XXXXXXX X XXXXXXXX XXXXXXX\n"
"             X                        X\n"
"             X                        X\n"
"             X                        X\n"
"             X                        X";

void interrupt time_handler(void)
{
	long slow_tick = time_ctr * 1193182 / (freq_div * 65536);
	time_ctr++;
	if (slow_tick != last_slow_tick)
	{
		//compensate for faster clock rate
		last_slow_tick = slow_tick;
		old_time_handler();
	}
	else
		outportb(0x20, 0x20);	//PIC, EOI
}

void opl2_out(unsigned char reg, unsigned char data)
{
	outportb(opl2_base, reg);
	outportb(opl2_base + 1, data);
	shadow_opl[reg] = data;
}

void opl2_clear(void)
{
	int i;
	for (i = 0; i < 256; i++)
		opl2_out(i, 0);
}

void puts_vga(char *text, int vgaaddr, unsigned char color)
{
	while (*text)
	{
		VGA[vgaaddr++] = *text;
		VGA[vgaaddr++] = color;
		text++;
	}
}

void mute_toggle(int channel)
{
	char buffer[2];
	unsigned char color;

	mutemask[channel] = !mutemask[channel];
	if (mutemask[channel])
	{
		opl2_out(0xB0 + channel, shadow_opl[0xB0 + channel] & 0xDF);
		color = C_NACT;
	}
	else
		color = C_LOW;

	puts_vga("-", XY(53, CHAN_Y + 1 + 2 * channel), color);
	puts_vga("----", XY(56, CHAN_Y + 1 + 2 * channel), color);
	puts_vga("---", XY(62, CHAN_Y + 1 + 2 * channel), color);
	puts_vga(dec[channel + 1], XY(51, CHAN_Y + 2 * channel),
		mutemask[channel] ? C_NACT : C_ACT);
}

int is_muted(int reg)
{
	if ((reg >= 0xA0 && reg <= 0xA8)
		|| (reg >= 0xB0 && reg <= 0xB8)
		|| (reg >= 0xC0 && reg <= 0xC8))
		return mutemask[reg & 0x0F];

	if ((reg >= 0x20 && reg <= 0x35)
		|| (reg >= 0x40 && reg <= 0x55)
		|| (reg >= 0x60 && reg <= 0x75)
		|| (reg >= 0x80 && reg <= 0x95)
		|| (reg >= 0xE0 && reg <= 0xF5))
		switch(reg % 32)
		{
			case 0: case 3: return mutemask[0];
			case 1: case 4: return mutemask[1];
			case 2: case 5: return mutemask[2];
			case 8: case 11: return mutemask[3];
			case 9: case 12: return mutemask[4];
			case 10: case 13: return mutemask[5];
			case 16: case 19: return mutemask[6];
			case 17: case 20: return mutemask[7];
			case 18: case 21: return mutemask[8];
		}
	return 0;
}

void print_cmd(unsigned char reg, unsigned char data)
{
	int channel_xlat[32] = {0, 2, 4, 1, 3, 5, ICL, ICL,
		6, 8, 10, 7, 9, 11, ICL, ICL,
		12, 14, 16, 13, 15, 17, ICL, ICL,
		ICL, ICL, ICL, ICL, ICL, ICL, ICL, ICL};
	char mult[16][4] = {"0.5", " 1 ", " 2 ", " 3 ", " 4 ", " 5 ", " 6 ", " 7 ",
		" 8 ", " 9 ", " 10", " 10", " 12", " 12", " 15", " 15"};
	char ksl[4][4] = {" - ", "1.5", " 3 ", " 6 "};
	char wave[4][5] = {" ^v ", " ^- ", " ^^ ", " // "};
	char buffer[16];

	int ch_line = channel_xlat[reg & 0x1F];
	int mch_line = reg & 0x0F;


	//raw data
	VGA[XY(3 * (reg & 0x0F) + 0, 22 + (reg >> 4))] = hex[data >> 4];
	VGA[XY(3 * (reg & 0x0F) + 1, 22 + (reg >> 4))] = hex[data & 0x0F];


	//decoded data
	switch (reg & 0xF0)
	{
		case 0x20: case 0x30:
			if (ch_line == ICL)
				break;

			puts_vga("AM", XY(2, ch_line + CHAN_Y), data & 0x80 ? C_HI : C_LOW);
			puts_vga("VIB", XY(5, ch_line + CHAN_Y), data & 0x40 ? C_HI : C_LOW);
			puts_vga("EGT", XY(9, ch_line + CHAN_Y), data & 0x20 ? C_HI : C_LOW);
			puts_vga("KSR", XY(13, ch_line + CHAN_Y), data & 0x10 ? C_HI : C_LOW);
			puts_vga(mult[data & 0x0F], XY(18, ch_line + CHAN_Y), C_NORM);
			break;

		case 0x40: case 0x50:	//total level
			if (ch_line == ICL)
				break;

			puts_vga(ksl[data >> 6], XY(24, ch_line + CHAN_Y), C_NORM);
			sprintf(buffer, "%5.2f",
				24.0f * ((data & 0x20) >> 5) +
				12.0f * ((data & 0x10) >> 4) +
				6.0f * ((data & 0x08) >> 3) +
				3.0f * ((data & 0x04) >> 2) +
				1.5f * ((data & 0x02) >> 1) +
				0.75f * (data & 0x01));
			puts_vga(buffer, XY(30, ch_line + CHAN_Y), C_NORM);
			break;

		case 0x60: case 0x70:	//AD
			if (ch_line == ICL)
				break;

			puts_vga(dec[data >> 4], XY(38, ch_line + CHAN_Y), C_NORM);
			puts_vga(dec[data & 0x0F], XY(41, ch_line + CHAN_Y), C_NORM);
			break;

		case 0x80: case 0x90:	//SR
			if (ch_line == ICL)
				break;

			puts_vga(dec[data >> 4], XY(44, ch_line + CHAN_Y), C_NORM);
			puts_vga(dec[data & 0x0F], XY(47, ch_line + CHAN_Y), C_NORM);
			break;

		case 0xA0:	//F-number
			if (mch_line > 8)
				break;

			sprintf(buffer, "%4d", ((shadow_opl[reg + 0x10] & 0x03) << 8) | data);
			puts_vga(buffer, XY(56, 2 * mch_line + 1 + CHAN_Y), C_NORM);
			break;

		case 0xB0:	//block, F-number
			if (mch_line > 8)
				break;

			puts_vga("KEY", XY(62, 2 * mch_line + 1 + CHAN_Y),
				data & 0x20 ? C_HI : C_LOW);
			sprintf(buffer, "%1d", (data >> 2) & 0x07);
			puts_vga(buffer, XY(53, 2 * mch_line + 1 + CHAN_Y), C_NORM);
			sprintf(buffer, "%4d", ((data & 0x03) << 8) | shadow_opl[reg - 0x10]);
			puts_vga(buffer, XY(56, 2 * mch_line + 1 + CHAN_Y), C_NORM);
			break;

		case 0xC0:	//feedback
			if (mch_line > 8)
				break;

			sprintf(buffer, "%1d", (data >> 1) & 0x07);
			puts_vga(buffer, XY(67, 2 * mch_line + 1 + CHAN_Y), C_NORM);
			puts_vga("CNT", XY(70, 2 * mch_line + 1 + CHAN_Y),
				data & 0x01 ? C_HI : C_LOW);
			break;

		case 0xE0: case 0xF0:
			if (ch_line == ICL)
				break;

			puts_vga(wave[data & 0x03], XY(75, ch_line + CHAN_Y), C_NORM);
			break;
	}
}

void print_delay(unsigned short delay)
{
	VGA[XY(16, 39)] = hex[(delay >> 12) & 0x0F];
	VGA[XY(17, 39)] = hex[(delay >> 8) & 0x0F];
	VGA[XY(18, 39)] = hex[(delay >> 4) & 0x0F];
	VGA[XY(19, 39)] = hex[delay & 0x0F];
}

void print_rem_delay(unsigned short rem)
{
	VGA[XY(16, 40)] = hex[(rem >> 12) & 0x0F];
	VGA[XY(17, 40)] = hex[(rem >> 8) & 0x0F];
	VGA[XY(18, 40)] = hex[(rem >> 4) & 0x0F];
	VGA[XY(19, 40)] = hex[rem & 0x0F];
}

void draw_logo(int x, int y)
{
	int r = 0, c = 0;
	char *text = logo;
	while (*text)
	{
		if (*text == '\n')
		{
			r++;
			c = 0;
		}
		else
		{
			VGA[XY(x + c, y + r)] = *text != ' ' ? 219 : ' ';
			VGA[XY(x + c++, y + r) + 1] = 15;
		}

		text++;
	}

	puts_vga("> imfplay 2.00 by kvee", XY(x + 15, y + 8), 10);
}

int main(int argc, char **argv)
{
	int i;
	unsigned long spd = 1193182;
	FILE *f;
	unsigned short len;
	unsigned char video_mode[11];


	//open IMF
	if (argc > 1)
	{
		f = fopen(argv[1], "rb");
		if (!f)
			return EXIT_FAILURE;

		if (argc > 2)
			freq_div = atoi(argv[2]);

		if (!(freq_div > 0 && freq_div < 32768))
			freq_div = 560;
	}
	else
	{
		puts("usage: imfplay <filename> [playback rate]\n"
			"       playback rate is in Hz, typical values are 560 (default), 700 and 280\n"
			"       keys [1]..[9] mute / unmute channels, [esc] quits the program");
		return EXIT_FAILURE;
	}


	//init video mode
	gettextinfo((struct text_info*)video_mode);
	textmode(C4350);
	clrscr();


	//prepare screen
	puts_vga("OPL2 register map", XY(0, 21), C_LOW);
	puts_vga("20+", XY(2, 0), C_LOW);
	puts_vga("40+", XY(24, 0), C_LOW);
	puts_vga("60+", XY(38, 0), C_LOW);
	puts_vga("80+", XY(44, 0), C_LOW);
	puts_vga("B0:A0+", XY(53, 0), C_LOW);
	puts_vga("C0+", XY(67, 0), C_LOW);
	puts_vga("E0+", XY(75, 0), C_LOW);
	puts_vga("multi scale  level   A  D  S  R     o F-num      fbk      wave",
	XY(17, 1),
	C_LOW);
	draw_logo(41, 39);
	{
		int reg;
		for (reg = 0; reg < 256; reg++)
		{
			VGA[XY(3 * (reg & 0x0F) + 0, 22 + (reg >> 4))] = 249;
			VGA[XY(3 * (reg & 0x0F) + 1, 22 + (reg >> 4))] = 249;
		}
	}


	for (i = 0; i < 18; i++)
	{
		puts_vga(i & 1 ? "2" : "1",
			XY(0, i + CHAN_Y),
			C_LOW);
		puts_vga("AM VIB EGT KSR [0.5] [ - ] [ 0.00]"
			" [ 0  0  0  0]",
			XY(2, i + CHAN_Y),
			C_LOW);
		if (i & 1)
			puts_vga(" [ ][    ] KEY [ ] CNT [    ]", XY(51, i + CHAN_Y), C_LOW);
		else
		{
			puts_vga(dec[i / 2 + 1], XY(51, i + CHAN_Y), C_ACT);
			puts_vga("[    ]", XY(74, i + CHAN_Y), C_LOW);
		}
	}


	//determine file type
	len = fgetc(f) << 8;
	len |= fgetc(f);
	if (len == 0)
	{
		puts_vga("type 0 file", XY(0, 39), C_NORM);
		fseek(f, 0, SEEK_SET);
	}
	else
		puts_vga("type 1 file", XY(0, 39), C_NORM);


	//setup timer speed
	old_time_handler = getvect(0x08);

	spd /= freq_div;
	outportb(0x43, 0x36);
	outportb(0x40, spd % 0x100);	//lo-byte
	outportb(0x40, spd / 0x100);	//hi-byte

	setvect(0x08, time_handler);


	//clear OPL2
	opl2_clear();


	//play
	{
		int run = 1;
		unsigned char rb[4];
		unsigned char reg, data;
		unsigned short delay;
		long next_event = time_ctr;

		while (run)
		{
			while (time_ctr < next_event)
			{
				if (kbhit())
				{
					int c = getch();
					if (c >= '1' && c <= '9')
						mute_toggle(c - '1');
					else if (c == 0x1B)
					{
						run = 0;
						break;
					}
				}
				print_rem_delay(next_event - time_ctr);
			}

			if (run == 0)
				break;

			if (fread(rb, sizeof(unsigned char), 4, f) != 4)
			{
				run = 0;
				break;
			}
			reg = rb[0];
			data = rb[1];
			delay = rb[2] | (rb[3] << 8);
			if (!is_muted(reg))
			{
				opl2_out(reg, data);
				print_cmd(reg, data);
			}
			print_delay(delay);

			next_event += delay;
		}
	}


	//clear OPL2
	opl2_clear();


	//restore timer speed
	outportb(0x43, 0x36);
	outportb(0x40, 0xFF);	//lo-byte
	outportb(0x40, 0xFF);	//hi-byte

	setvect(0x08, old_time_handler);


	//restore video mode
	textmode(video_mode[6]);


	//exit
	return EXIT_SUCCESS;
}