#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <bios.h>
#include <dos.h>

#include "binfile.h"
#include "emucfg.h"
#include "sidtune.h"
#include "6510_.h"
#include "mytypes.h"
#include "ptypes.h"
#include "myendian.h"




#define VERSION				"0.1"

#define DEFAULT_CALLS		50
#define DEFAULT_SID_PORT 	0x280

// 8253 timer
#define TIMER_8253_HZ		1193180

#define TIMER_CHN0_DOS		0xffff

#define TIMER_REG_CHN0		0x40
#define TIMER_REG_CONTROL	0x43

#define PIC_REG_EOI			0x20

// key scan codes
#define VK_ESCAPE			0x01
#define VK_P				0x19
#define VK_F				0x21
#define VK_C				0x2e
#define VK_N				0x31
#define VK_SPACE			0x39



// SID section
extern void sidReset();
extern void sidOutp(uword reg, ubyte c);

ubyte playRamRom;


// Runtime section

// 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 bRuntimePause;

static boolm parVerbose;
static boolm parQuiet;

static uword timerCounter;
static udword timerTicks;
static boolm timerSync;
void interrupt (*dosTimerIsr)(...);



// 8253 timer helpers

void interrupt runtimeTimerIsr(...) {
	_disable();

	timerSync = true;

	timerTicks += timerCounter;
	if (timerTicks >= TIMER_CHN0_DOS) {
		timerTicks -= TIMER_CHN0_DOS;
		_enable();
		_chain_intr(dosTimerIsr);
	} else {
		// accept interrupt
		outp(PIC_REG_EOI, 0x20);
		_enable();
	}
}


void timerUpdate()
{
	timerCounter = (udword)TIMER_8253_HZ / calls;

	_disable();

	outp(TIMER_REG_CONTROL, 0x3c);
	outp(TIMER_REG_CHN0, timerCounter & 0x00ff);
	outp(TIMER_REG_CHN0, (timerCounter >> 8) & 0x00ff);

	timerTicks = 0;

	_enable();
}


static void timerSetup()
{
	_disable();

	dosTimerIsr = _dos_getvect(0x08);
	_dos_setvect(0x08, runtimeTimerIsr);

	_enable();

	timerUpdate();
}


static void timerRestore()
{
	_disable();

	_dos_setvect(0x08, dosTimerIsr);

	outp(TIMER_REG_CONTROL, 0x3c);
	outp(TIMER_REG_CHN0, 0xff);
	outp(TIMER_REG_CHN0, 0xff);

	_enable();
}



// sid player runtime subs

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 = 1000 / calls;
}


static void runtimeUpdateReplayingSpeed()
{
	if (timer != readLEword(c64mem2 + 0xdc04)) {
		timer = readLEword(c64mem2 + 0xdc04);
		if (timer < 16) {
			timer = defaultTimer;
		}
		calls = sidtuneClockSpeed / timer;
		period = 1000 / calls;

		timerUpdate();
	}
}



static void timerproc(emuEngine &thisEmu, sidTune &thistune)
{
	// Ensure a sane status of the whole emulator.
	if (thisEmu.returnStatus() && thistune.returnStatus()) {
		uword replayPC = thistune.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
			}
		}

		//char retcode =
		interpreter(replayPC, playRamRom, 0, 0, 0);

		if (thistune.returnSongSpeed() == 0) {
			runtimeUpdateReplayingSpeed();
		}

		// put register values to SID chip
		if (sidKeysOn[0x04] && !(c64mem2[0xd404] & 0x01)) {
			sidOutp(0x04, c64mem2[0xd404] | 0x01);
		}
		if (sidKeysOff[0x04] && (c64mem2[0xd404] & 0x01)) {
			sidOutp(0x04, c64mem2[0xd404] & 0xfe);
		}

		if (sidKeysOn[0x0b] && !(c64mem2[0xd40b] & 0x01)) {
			sidOutp(0x0b, c64mem2[0xd40b] | 0x01);
		}
		if (sidKeysOff[0x0b] && (c64mem2[0xd40b] & 0x01)) {
			sidOutp(0x0b, c64mem2[0xd40b] & 0xfe);
		}

		if (sidKeysOn[0x12] && !(c64mem2[0xd412] & 0x01)) {
			sidOutp(0x12, c64mem2[0xd412] | 0x01);
		}
		if (sidKeysOff[0x12] && (c64mem2[0xd412] & 0x01)) {
			sidOutp(0x12, c64mem2[0xd412] & 0xfe);
		}

		for (int i = 0; i <= 24; i ++) {
			sidOutp(i, c64mem2[0xd400 + i]);
		}
	} // end if status
}



int c_break(void)
{
	if (!bRuntimeCont) {
		// emergency exit
		timerRestore();
		return 0;
	}
	bRuntimeCont = false;
	return 1;
}


static void updatePlayInfo(int song, int numSongs,
							udword time, boolm pause,
							int timerError, boolm showTimerError)
{
	printf("\r%s %d of %d : %ldm %02ld.%lds",
		(pause ? " PAUSE " : "Playing"),
		song, numSongs,
		(time / 1000) / 60,
		(time / 1000) % 60,
		(time / 100) % 10);
	if (showTimerError) printf(", TE %d", timerError);
}


static boolm openPlayer(const char *filename, uword sidPort) {
	binfile file;
	emuEngine *emu;
	emuConfig *conf;
	sidTune *tune;
	struct sidTuneInfo *tuneInfo;
	udword timeCount, timeScreenUpdate, skipTimeCount;
	int timerError;
	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);

		file.close();
		delete tuneInfo;
		delete tune;
		delete emu;
		return false;
	}

	conf = new emuConfig;
	if (conf == NULL) {
		file.close();
		delete tuneInfo;
		delete tune;
		delete emu;
		return false;
	}
	emu->getConfig(*conf);
	conf->sidPort = sidPort;
	if (!emu->setConfig(*conf)) {
		printf("ERROR: Invalid config\n");

		file.close();
		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");

		file.close();
		delete tuneInfo;
		delete tune;
		delete emu;
		return false;
	}

	timeCount = timeScreenUpdate = 0;
	skipTimeCount = 0;
	timerError = 0;

	// show tune info
	tune->returnInfo(*tuneInfo);
	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, Speed: %s\n",
				tuneInfo->formatString, tuneInfo->speedString);
			printf("Clock: %ldHz, Calls: %d, Default Timer: %d, Timer: %d, Period: %ldms\n",
				sidtuneClockSpeed, calls, defaultTimer, timer, period);
			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);
		}
	}

	// setup 8253 timer according tune settings
	timerSetup();

	// start main loop
	ret = true;

	ctrlbrk(c_break);

	bRuntimeCont = true;
	bRuntimePause = false;

	timerSync = false;

	for (;;) {
		if (!bRuntimePause) {
			timerproc(*emu, *tune);
			timeCount += period;
		}

		// sync with system timer
		if (skipTimeCount <= timeCount) {
			do {} while (!timerSync);
			timerSync = false;
		}

		if (!bRuntimeCont) break;

		if (!parQuiet && (timeScreenUpdate <= timeCount)) {
			updatePlayInfo(tuneInfo->currentSong, tuneInfo->songs,
				timeCount, bRuntimePause, timerError, parVerbose);
			timeScreenUpdate += 100;
		}

		// processed console keys
		if (_bios_keybrd(_KEYBRD_READY) > 0) {
			unsigned int virtualKey = _bios_keybrd(_KEYBRD_READ);

			switch (virtualKey >> 8) {
			case VK_ESCAPE:
				bRuntimeCont = false;
				break;
			case VK_C:
				// Ctrl-C
				if (_bios_keybrd(_KEYBRD_SHIFTSTATUS) & 0x04) {
					bRuntimeCont = false;
				}
				break;
			case VK_SPACE:
			case VK_P:
				if (!bRuntimePause) {
					// reset SID regs
					sidReset();
				}
				bRuntimePause = !bRuntimePause;

				if (!parQuiet) {
					updatePlayInfo(tuneInfo->currentSong, tuneInfo->songs,
						timeCount, bRuntimePause, timerError, parVerbose);
				}
				break;
			case VK_F:
				if (!bRuntimePause) {
					skipTimeCount = timeCount + 5 * 1000;	// 5s
				}
				break;
			case VK_N:
				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;
					break;
				}

				// tune timer settings may have changed
				timerUpdate();

				timeCount = timeScreenUpdate = 0;
				timerError = 0;
				skipTimeCount = 0;
				tune->returnInfo(*tuneInfo);
				printf("\r                                                  ");
				break;
			}
		}

		// timer interupt occured before the current interrupt was processed
		if (skipTimeCount <= timeCount && timerSync) timerError++;
	}
	if (!parQuiet) printf("\n");

	// resore 8253 timer to DOS defaults
	timerRestore();

	file.close();
	delete tuneInfo;
	delete tune;
	delete emu;

	sidReset();

	return ret;
}


void version()
{
	printf("Open SID Player for Innovation SSI-2001 v%s\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.exe /?\n");
	printf("       sidplay.exe /I\n");
	printf("       sidplay.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("  /P - SSI-2001 base port: 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");
}



int main(int argc, char *argv[])
{
	uword parSidPort;
	char *parFilename;

	parSidPort = DEFAULT_SID_PORT;
	parQuiet = false;
	parVerbose = false;

	int i;
	for (i = 1; i < argc; i++) {
		if (argv[i][0] == '/' && argv[i][2] == '\0') {
			switch (tolower(argv[i][1])) {
			case 'q':
				parQuiet = true;
				break;
			case 'p':
				if (++i >= argc) break;
				if (argv[i][0] == '0' && tolower(argv[i][1] == 'x'))
					parSidPort = strtoul(argv[i], NULL, 16);
				else
					parSidPort = strtoul(argv[i], NULL, 10);
				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];

	if (!openPlayer(parFilename, parSidPort)) {
		return -1;
	}

	return 0;
}

