#include <windows.h>
#include <mmsystem.h>

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

#include "binfile.h"
#include "eeconfig.h"
#include "sidtune.h"
#include "6510_.h"
#include "6581_.h"
#include "myendian.h"



#define VERSION				"1.0"

#define DEFAULT_CALLS			50

#define DEFAULT_SID1_PORT 		0x280
#define DEFAULT_SID2_PORT 		0
#define DEFAULT_SID3_PORT 		0



#define spinlockInit(_sl)		(_sl) = 0
#define spinlockLock(_sl)		\
	do {} while (InterlockedExchange(&(_sl), 1) == 1)
#define spinlockUnlock(_sl)		(_sl) = 0


typedef struct {
	LONG spinlock;

	emuEngine *emu;
	sidTune *tune;
	struct sidTuneInfo *tuneInfo;

	uword wDisChan;

	DWORD dwNextTime;
	DWORD dwPeriodMsErr;
	DWORD dwTimeCount;
	DWORD dwSkipTimeCount;
	int timerError;

	UINT uTimerID;
} runtimeParameter_t;



// Runtime section

ubyte playRamRom;

// Song clock speed (PAL or NTSC). Does not affect pitch.
static udword sidtuneClockSpeed;

static uword calls = DEFAULT_CALLS;
static uword timer, defaultTimer;
static udword period;	// timer period (1000us / calls)

static boolm bRuntimeCont;

static boolm parVerbose;
static boolm parQuiet;
static uword parDisChan;


void runtimeSetReplayingSpeed(int clockMode, uword callsPerSec)
{
	switch (clockMode) {	case SIDTUNE_CLOCK_NTSC:		sidtuneClockSpeed = 1022727;        	timer = (defaultTimer = 0x4295);	        break;	case SIDTUNE_CLOCK_PAL:	default:		sidtuneClockSpeed = 985248;		timer = (defaultTimer = 0x4025);		break;	}
	switch (callsPerSec) {	case SIDTUNE_SPEED_CIA_1A:		timer = readLEword(c64mem2 + 0xdc04);		if (timer < 16) {			timer = defaultTimer;		}		calls = sidtuneClockSpeed / timer;		break;	default:		calls = callsPerSec;	}
	period = 1000000 / calls;}static void runtimeUpdateReplayingSpeed()
{
	if (timer != readLEword(c64mem2 + 0xdc04)) {		timer = readLEword(c64mem2 + 0xdc04);		if (timer < 16) {
			timer = defaultTimer;		}		calls = sidtuneClockSpeed / timer;		period = 1000000 / calls;	}}



void CALLBACK timerCallback(UINT wTimerID, UINT msg, DWORD dwUser, DWORD dw1, DWORD dw2)
{
	runtimeParameter_t *parameter = (runtimeParameter_t *)dwUser;
	DWORD dwPeriodMs;
	UINT uSleepTime;

	spinlockLock(parameter->spinlock);

	do {
		// ensure a sane status of the whole emulator.
		if (parameter->emu->returnStatus() && parameter->tune->returnStatus()) {
			uword replayPC = parameter->tune->returnPlayAddr();

			// playRamRom was set by external player interface.
			if (replayPC == 0 ) {
				playRamRom = c64mem1[1];
				if ((playRamRom & 2) != 0) {	// isKernal ?
					replayPC = readLEword(c64mem1 + 0x0314);	// IRQ
				} else {
					replayPC = readLEword(c64mem1 + 0xfffe);	// NMI
				}
			}

			spinlockUnlock(parameter->spinlock);

			interpreter(replayPC, playRamRom, 0, 0, 0);

			spinlockLock(parameter->spinlock);

			if (parameter->tune->returnSongSpeed() == SIDTUNE_SPEED_CIA_1A) {
				runtimeUpdateReplayingSpeed();
			}
		}


		dwPeriodMs = period / 1000;
		parameter->dwPeriodMsErr += dwPeriodMs % 1000;
		if (parameter->dwPeriodMsErr > 1000) {
			dwPeriodMs += parameter->dwPeriodMsErr / 1000;
			parameter->dwPeriodMsErr = parameter->dwPeriodMsErr % 1000;
		}

		parameter->dwTimeCount += dwPeriodMs;
	} while (parameter->dwSkipTimeCount > parameter->dwTimeCount);


	parameter->dwNextTime += dwPeriodMs;
	DWORD dwCurTime = timeGetTime();

	if (parameter->dwNextTime > dwCurTime) {
		uSleepTime = parameter->dwNextTime - dwCurTime;
	} else {
		uSleepTime = 1;
		parameter->timerError++;
	}

	parameter->uTimerID = timeSetEvent(uSleepTime, 1, timerCallback,
		dwUser, TIME_ONESHOT);

	spinlockUnlock(parameter->spinlock);
}


BOOL timerStart(runtimeParameter_t *parameter)
{
   	spinlockLock(parameter->spinlock);
	parameter->dwNextTime = timeGetTime();
	parameter->dwPeriodMsErr = 0;
	parameter->uTimerID = 0;
   	spinlockUnlock(parameter->spinlock);

	timerCallback(0, TIME_ONESHOT, (DWORD)parameter, 0, 0);

   	spinlockLock(parameter->spinlock);
	if (parameter->uTimerID == 0) {
		spinlockUnlock(parameter->spinlock);
		return FALSE;
	}
	spinlockUnlock(parameter->spinlock);

	return TRUE;
}


void timerStop(runtimeParameter_t *parameter)
{
	if (parameter->uTimerID != 0) {
	   	spinlockLock(parameter->spinlock);
		timeKillEvent(parameter->uTimerID);
        parameter->uTimerID = 0;
	   	spinlockUnlock(parameter->spinlock);
    }
}



static WORD GetConsoleVirtualKeyCode()
{
	HANDLE hConsoleInput;
	INPUT_RECORD InputRecord;
	DWORD dwNum;

	hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);
	if (hConsoleInput == INVALID_HANDLE_VALUE) {
		return 0;
	}

	if (!GetNumberOfConsoleInputEvents(hConsoleInput, &dwNum) ||  dwNum < 1) {
		return 0;
	}

	ReadConsoleInput(hConsoleInput, &InputRecord, 1, &dwNum);

	if (InputRecord.EventType == KEY_EVENT &&
			InputRecord.Event.KeyEvent.bKeyDown == TRUE) {
		return InputRecord.Event.KeyEvent.wVirtualKeyCode;
	}

	return 0;
}


static void clearLine()
{
	printf("\r                                        \r");
}


static void updateTuneInfo(struct sidTuneInfo &tuneInfo)
{
	clearLine();
	printf("Tune %d of %d, Clock: %dHz, Speed: %s\n",
    	tuneInfo.currentSong, tuneInfo.songs,
        sidtuneClockSpeed, tuneInfo.speedString);
	printf("Calls: %d, Default Timer: %d, Timer: %d, Period: %dms\n",
		calls, defaultTimer, timer, period);
}


static void updatePlayInfo(int song, int numSongs,
							int time, boolm pause,
							int timerError, boolm showTimerError)
{
	printf("\r%s %d of %d : %dm %02d.%ds",
		(pause ? " PAUSE " : "Playing"),
    	song, numSongs,
		(time / 1000) / 60,
		(time / 1000) % 60,
		(time / 100) % 10);
	if (showTimerError) printf(", TE %d", timerError);
}


static void updateChanInfo(ubyte sidChips, uword wDisChan)
{
	clearLine();
	printf("Cnannels: [%c %c %c]",
    	CHAN_DISABLED(wDisChan, 0) ? '-' : '1',
        CHAN_DISABLED(wDisChan, 1) ? '-' : '2',
        CHAN_DISABLED(wDisChan, 2) ? '-' : '3');
	if (sidChips >= 2) {
		printf(" [%c %c %c]",
    		CHAN_DISABLED(wDisChan, 3) ? '-' : '4',
        	CHAN_DISABLED(wDisChan, 4) ? '-' : '5',
	        CHAN_DISABLED(wDisChan, 5) ? '-' : '6');
	}
	if (sidChips == 3) {
		printf(" [%c %c %c]",
    		CHAN_DISABLED(wDisChan, 6) ? '-' : '7',
        	CHAN_DISABLED(wDisChan, 7) ? '-' : '8',
	        CHAN_DISABLED(wDisChan, 8) ? '-' : '9');
	}

	printf(", Filters: [%c", FILTER_DISABLED(wDisChan, 0) ? '-' : '1');
	if (sidChips >= 2) printf(" %c", FILTER_DISABLED(wDisChan, 1) ? '-' : '2');
	if (sidChips == 3) printf(" %c", FILTER_DISABLED(wDisChan, 2) ? '-' : '3');

	printf("]\n");
}



static boolm openPlayer(const char *filename, uword *sidPorts)
{
	binfile file;
	emuEngine *emu;
	emuConfig *conf;
	sidTune *tune;
	struct sidTuneInfo *tuneInfo;
	HANDLE hThread;
	DWORD threadId;
	runtimeParameter_t parameter;
	boolm updateClear, updateChan;
	boolm pseudoStereo;
	boolm bPause;
	boolm ret;

	emu = new emuEngine;
	if (emu == NULL) {
		return false;
	}

	tune = new sidTune;
	if (tune == NULL) {
		delete emu;
		return false;
	}

	tuneInfo = new sidTuneInfo;
	if (tuneInfo == NULL) {
		delete tune;
		delete emu;
		return false;
	}

	if (file.open(filename, binfile::modeopen | binfile::moderead)) {
		printf("ERROR: Can't open file %s\n", filename);
		delete tuneInfo;
		delete tune;
		delete emu;
		return false;
	}

	if (!tune->open(file)) {
		tune->returnInfo(*tuneInfo);
		printf("%s\n", tuneInfo->statusString);

		delete tuneInfo;
		delete tune;
		delete emu;
		file.close();
		return false;
	}

	file.close();

	// set SID chips
	tune->returnInfo(*tuneInfo);
	if (tuneInfo->sidChips == 1 && (sidPorts[1] != 0 || sidPorts[2] != 0)) {
		// Pseudo Stereo mode
		c64setSidChip(0, 0xd400, emu->writeDataSID_PS);
		pseudoStereo = true;
	} else {
		int sidChipIdx = 1;
		c64setSidChip(0, 0xd400, emu->writeDataSID);
		if (tuneInfo->secondSidAddress != 0) {
			c64setSidChip(sidChipIdx++, tuneInfo->secondSidAddress,
        		emu->writeDataSID);
		}
		if (tuneInfo->thirdSidAddress != 0) {
			c64setSidChip(sidChipIdx++, tuneInfo->thirdSidAddress,
        		emu->writeDataSID);
		}
		pseudoStereo = false;
	}

	conf = new emuConfig;
	if (conf == NULL) {
		delete tuneInfo;
		delete tune;
		delete emu;
		return false;
	}
	emu->getConfig(*conf);
	for (int i = 0; i < SID_NUM_MAX; i++) {
		conf->sidPorts[i] = sidPorts[i];
	}
	if (!emu->setConfig(*conf)) {
		printf("ERROR: Invalid config\n");

		delete conf;
		delete tuneInfo;
		delete tune;
		delete emu;
		return false;
	}
	delete conf;

	if (!sidEmuInitializeSong(*emu, *tune, 0)) {
		printf("ERROR: Can't initialize start song\n");

		delete tuneInfo;
		delete tune;
		delete emu;
		return false;
	}

	tune->returnInfo(*tuneInfo);

	// show tune info
	if (!parQuiet) {
		if (parVerbose) printf("File:      %s\n", filename);
			printf("Name:      %s\n", tuneInfo->nameString);
			printf("Author:    %s\n", tuneInfo->authorString);
			printf("Copyright: %s\n", tuneInfo->copyrightString);
		if (parVerbose) {
			for (int i = 0; i < tuneInfo->numberOfCommentStrings; i++) {
				printf("%s\n", tuneInfo->commentString[i]);
			}
		}

		if (parVerbose) {
			printf("Format: %s, SID chips: %d%s\n",
				tuneInfo->formatString, tuneInfo->sidChips,
					pseudoStereo ? ", Pseudo Stereo" : "");
			printf("loadAddr: 0x%x, initAddr 0x%x, playAddr 0x%x0, irqAddr: 0x%x\n",
				tuneInfo->loadAddr, tuneInfo->initAddr, tuneInfo->playAddr, tuneInfo->irqAddr);
			printf("musPlayer: %d\n", tuneInfo->musPlayer);
			updateTuneInfo(*tuneInfo);
		}
	}

   	emu->setDisChan(parDisChan);

	// start main loop
	spinlockInit(parameter.spinlock);
	parameter.emu = emu;
	parameter.tune = tune;
	parameter.tuneInfo = tuneInfo;
	parameter.dwTimeCount = 0;
	parameter.timerError = 0;
	parameter.dwSkipTimeCount = 0;
	parameter.wDisChan = parDisChan;

	timerStart(&parameter);

	ret = true;
	updateClear = false;
	updateChan = false;
	bRuntimeCont = true;
	bPause = false;

	for (;;) {
		Sleep(100);

		if (!bRuntimeCont) break;

		if (updateChan) {
			emu->setDisChan(parameter.wDisChan);
		}

		if (!parQuiet) {
			spinlockLock(parameter.spinlock);
			DWORD dwTimeCount = parameter.dwTimeCount;
			int timerError = parameter.timerError;
			spinlockUnlock(parameter.spinlock);

			if (updateClear) {
				clearLine();
				updateClear = false;
			}

			if (updateChan) {
				updateChanInfo(tuneInfo->sidChips, parameter.wDisChan);
				updateChan = false;
			}

			updatePlayInfo(tuneInfo->currentSong, tuneInfo->songs,
			dwTimeCount, bPause, timerError, parVerbose);
		}

		// processed console keys
		int n = 0;
		WORD wVirtualKey = GetConsoleVirtualKeyCode();
		switch (wVirtualKey) {
		case VK_ESCAPE:
			bRuntimeCont = false;
			break;
		case VK_SPACE:
		case /*VK_P*/'P':
			{
				bPause = !bPause;
				if (!bPause) {
					emu->resumeSID();
					timerStart(&parameter);
				} else {
					timerStop(&parameter);
					emu->resetSID();
				}
			}
			break;
		case /*VK_F*/'F':
			spinlockLock(parameter.spinlock);
			if (!bPause) {
				parameter.dwSkipTimeCount =
					parameter.dwTimeCount + 5 * 1000;	// 5s
			}
			spinlockUnlock(parameter.spinlock);
			break;
		case /*VK_N*/'N':
			timerStop(&parameter);
			spinlockLock(parameter.spinlock);
			if (sidEmuInitializeSong(*emu, *tune,
					(tuneInfo->currentSong % tuneInfo->songs) + 1) == false) {
				printf("\nERROR: Can't initialize song %d\n",
					(tuneInfo->currentSong % tuneInfo->songs) + 1);
				bRuntimeCont = false;
				ret = false;
			}
			parameter.dwTimeCount = 0;
			parameter.timerError = 0;
			parameter.dwSkipTimeCount = 0;
			tune->returnInfo(*tuneInfo);
			spinlockUnlock(parameter.spinlock);
			timerStart(&parameter);
			updateClear = true;
			break;
		case /*VK_?*/0xbb:
			n++;
		case /*VK_?*/0xbd:
			n++;
		case /*VK_0*/'0':
			spinlockLock(parameter.spinlock);
			if (FILTER_DISABLED(parameter.wDisChan, n)) {
				ENABLE_FILTER(parameter.wDisChan, n);
			} else {
				DISABLE_FILTER(parameter.wDisChan, n);
			}
			spinlockUnlock(parameter.spinlock);
			updateChan = true;
			break;
		case /*VK_1*/'1':
		case /*VK_2*/'2':
		case /*VK_3*/'3':
		case /*VK_3*/'4':
		case /*VK_3*/'5':
		case /*VK_3*/'6':
		case /*VK_3*/'7':
		case /*VK_3*/'8':
		case /*VK_3*/'9':
			n = wVirtualKey - '1';
			spinlockLock(parameter.spinlock);
			if (CHAN_DISABLED(parameter.wDisChan, n)) {
				ENABLE_CHAN(parameter.wDisChan, n);
			} else {
				DISABLE_CHAN(parameter.wDisChan, n);
			}
			spinlockUnlock(parameter.spinlock);
			updateChan = true;
			break;
		}
	}
	if (!parQuiet) printf("\n");

	timerStop(&parameter);

	emu->reset();

	delete tuneInfo;
	delete tune;
	delete emu;

	return ret;
}


static BOOL ctrl_handler(DWORD dwCtrlType)
{
	switch (dwCtrlType) {
	case CTRL_C_EVENT:
		bRuntimeCont = false;
		return TRUE;
	default:
		return FALSE;
	}
}


static uword parsePort(const char *str) {
	if (str[0] == '0' && tolower(str[1] == 'x'))
		return strtoul(str, NULL, 16);
   	else
		return strtoul(str, NULL, 10);
}


void version()
{
   	printf("Open SID Player for Innovation SSI-2001 v%s (Win9x)\n", VERSION);
	printf("(C)2018 Alexander Ozumenko <scg@stdio.ru>\n");
   	printf("Based on Open Cubic Player 2.6.0pre6\n");
}


void usage()
{
	printf("usage: sidplay-win.exe /?\n");
	printf("       sidplay-win.exe /I\n");
	printf("       sidplay-win.exe [/Q] [/V] [/P <port>] <file>\n");
	printf("\n");
	printf("  /? - show usage\n");
	printf("  /I - show program version\n");
	printf("  /Q - enable quiet mode\n");
	printf("  /V - enable verbose messages\n");
	printf("  /C n1[:n2[:n3[:...]]] - disable channels: 1-9\n");
	printf("  /F n1[:f2[:f3]] - disable filters: 1-3\n");
	printf("  /P port1[:port2[:port3]] - SSI-2001 base port(s):\n");
	printf("      0x280, 0x2A0, 0x2C0, 0x2E0\n");
	printf("\n");
	printf("Runtime keys:\n");
	printf("  <ECS>, <Ctrl-C> - exit\n");
	printf("  <P>, <Space> - play/pause\n");
	printf("  <N> - next song\n");
	printf("  <F> - fast forward 5s\n");
	printf("  <1> - <9> - disable/enable channels\n");
	printf("  <0>, <->, <=> - disable/enable filters\n");
}


int main(int argc, char *argv[], char *envp[])
{
	uword parSidPorts[3];
	char *parFilename;

	parSidPorts[0] = DEFAULT_SID1_PORT;
	parSidPorts[1] = DEFAULT_SID2_PORT;
	parSidPorts[2] = DEFAULT_SID3_PORT;
	parQuiet = false;
	parVerbose = false;
	parDisChan = 0;

	int i;
	for (i = 1; i < argc; i++) {
		if (argv[i][0] == '/' && argv[i][2] == '\0') {
			char c = tolower(argv[i][1]);
			switch (c) {
			case 'c':
				{
	         			if (++i >= argc) break;
					char *ptr = argv[i];
					char *ptr_next, *ptr_space;
					do {
						int n = strtoul(ptr, NULL, 10);
						if (n >= 1 && n <= 9) {
							DISABLE_CHAN(parDisChan, n - 1);
						}
						ptr_space = strchr(ptr, ' ');
						ptr_next = strchr(ptr, ':');
						ptr = ptr_next + 1;
					} while (ptr_next != NULL &&
					(ptr_space == NULL || ptr_next < ptr_space));
				}
			break;
			case 'f':
            			{
					if (++i >= argc) break;
					char *ptr = argv[i];
					char *ptr_next, *ptr_space;
					do {
						int n = strtoul(ptr, NULL, 10);
						if (n >= 1 && n <= 3) {
							DISABLE_FILTER(parDisChan, n - 1);
						}
						ptr_space = strchr(ptr, ' ');
						ptr_next = strchr(ptr, ':');
						ptr = ptr_next + 1;
					} while (ptr_next != NULL &&
						(ptr_space == NULL || ptr_next < ptr_space));
				}
				break;
			case 'q':
				parQuiet = true;
				break;
			case 'p':
				{
					if (++i >= argc) break;
					char *ptr = argv[i];
					for (int j = 0; j < 3; j++) {
						char *ptr_next, *ptr_space;
						parSidPorts[j] = parsePort(ptr);
						ptr_space = strchr(ptr, ' ');
						ptr_next = strchr(ptr, ':');
						ptr = ptr_next + 1;
						if (parSidPorts[j] == 0 || ptr_next == NULL ||
								(ptr_space != NULL && ptr_next > ptr_space)) {
							break;
						}
					}
				}
				break;
			case 'v':
				parVerbose = true;
				break;
			case 'i':
				version();
				return 0;
			case '?':
			default:
				usage();
				return -1;
			}
		} else {
			break;
		}
	}

	if (i == argc) {
		usage();
		return -1;
	}

	parFilename = argv[i];

	SetConsoleCtrlHandler((PHANDLER_ROUTINE)ctrl_handler, TRUE);

	if (!openPlayer(parFilename, parSidPorts)) {
		return -1;
	}

	return 0;
}

