
/*
 * jsCHIP-8 v0.1
 * 
 * Copyright (c) 2006-2008 Jacob Seidelin, cupboy@gmail.com
 * MIT License [http://www.opensource.org/licenses/mit-license.php]
 * 
 */

(function() {

var C8;

function init() {

	var oList = document.getElementById("chip8romlist");

	for (r=0;r<aROMList.length;r++) {
		var oROM = aROMList[r];

		var oOption = document.createElement("option");
		oOption.text = oROM.name;
		oOption.value = oROM.file;
		oOption.description = oROM.desc;

		oList.options[r] = oOption;
	}

	oList.onchange = function() {
		var oDesc = document.getElementById("chip8romdesc");
		var oSelected = oList.options[oList.selectedIndex];
		if (oSelected) {
			oDesc.innerHTML = oSelected.description;
		} else {
			oDesc.innerHTML = "";
		}
	}

	var oBtn = document.getElementById("chip8loadbutton");
	addEvent(oBtn, "click", loadROMData);

	C8 = new Chip8Emu();

	var oScr = new Chip8ScreenHTML(document.getElementById("chip8screenspace"));
	oScr.setScale(2);

	C8.setScreen(oScr);

	addEvent(window, "unload", function() { C8.cleanup(); });
}

function HTTPRequest(strURL, fncCallBack)
{
	var oHTTP = null;
	if (window.XMLHttpRequest) {
		oHTTP = new XMLHttpRequest();
	} else if (window.ActiveXObject) {
		oHTTP = new ActiveXObject("Microsoft.XMLHTTP");
	}
	if (oHTTP) {
		if (fncCallBack) {
			if (typeof(oHTTP.onload) != "undefined")
				oHTTP.onload = function() {
					fncCallBack(this);
				};
			else {
				oHTTP.onreadystatechange = function() {
					if (this.readyState == 4) {
						fncCallBack(this);
					}
				};
			}
		}
		oHTTP.open("GET", strURL, true);
		oHTTP.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
		oHTTP.send(null);
	}

}

function hex(iNum)
{
	return iNum.toString(16);
}

function addEvent(oObject, strEvent, fncAction) {
	if (oObject.addEventListener) { 
		oObject.addEventListener(strEvent, fncAction, false); 
	} else if (oObject.attachEvent) { 
		oObject.attachEvent("on" + strEvent, fncAction); 
	}
}

function wrtdbg(str, bNewLine) {
	document.getElementById("chip8debug").value += str + (bNewLine ? "" : "\r\n");
}

function loadROMData() {
	var oList = document.getElementById("chip8romlist");

	if (!oList.value) return;
	var strROM = oList.value;

	HTTPRequest("./parserom.php?rom=" + strROM, ROMDataLoaded);
}

function ROMDataLoaded(HTTP) {
	var bytes = eval(HTTP.responseText);
	C8.loadROM(bytes);
}


// begin emulator code

function Chip8Emu()
{
	this._bRunning = false;
	this._bPaused = false;
	this._bWaiting = false;

	this._aRAM = new Array();
	this._aStack = new Array();

	this._iKeyPressed = 0;
	this._bKeyIsDown = false;

	this._iOP1 = 0;
	this._iOP2 = 0;
	this._iOP3 = 0;
	this._iOP4 = 0;

	this._bDebug = false;
	this._bHigh = false;

	// keys
	this._aKeys = new Array();
	this._aKeys[1] = 103;	// 7
	this._aKeys[2] = 104;	// 8
	this._aKeys[3] = 105;	// 9
	this._aKeys[4] = 100;	// 4
	this._aKeys[5] = 101;	// 5
	this._aKeys[6] = 102;	// 6
	this._aKeys[7] = 97;	// 1
	this._aKeys[8] = 98;	// 2
	this._aKeys[9] = 99;	// 3
	this._aKeys[10] = 96;	// 0
	this._aKeys[11] = 45;	// Insert
	this._aKeys[12] = 46;	// Delete
	this._aKeys[13] = 36;	// Home
	this._aKeys[14] = 25;	// End
	this._aKeys[15] = 33;	// PageUp
	this._aKeys[16] = 34;	// PageDown

	// registers
	this._PC = 0; // pgm count
	this._SP = 0; // stack pointer
	this._DT = 0; // delay timer
	this._ST = 0; // sound timer
	this._V = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; // general registers
	this._I = 0; // index

	this._oScreen = null;

	var me = this;

	addEvent(document, "keydown", 
		function(e) {me._onKeyDown(e ? e : event); return true;}
	);

	addEvent(document, "keyup", 
		function(e) {me._onKeyUp(e ? e : event); return true;}
	);

	if (this._bDebug) {
		if (document.getElementById("chip8debug")) {
			document.getElementById("chip8debug").style.display = "block";
		}
	}
}

Chip8Emu.prototype.setScreen = function(oScr)
{
	this._oScreen = oScr;
}

Chip8Emu.prototype.cleanup = function()
{
	// should clean up event handlers here
}

Chip8Emu.prototype.reset = function()
{
	this._initRAM();
	this._PC = 0x200;
	this._opSetLow();
}

Chip8Emu.prototype._cls = function()
{
	this._oScreen.clear();
}

Chip8Emu.prototype._initScreen = function()
{
	this._oScreen.init();
}

Chip8Emu.prototype._initRAM = function()
{
	// zero RAM
	for (var i=0;i<0x2000;i++)
		this._aRAM[i] = 0;


	// load hex font
	var aFont = new Array(
				0xF0, 0x90, 0x90, 0x90, 0xF0,	// 0 
				0x20, 0x60, 0x20, 0x20, 0x70,	// 1
				0xF0, 0x10, 0xF0, 0x80, 0xF0,	// 2
				0xF0, 0x10, 0xF0, 0x10, 0xF0,	// 3
				0x90, 0x90, 0xF0, 0x10, 0x10, 	// 4
				0xF0, 0x80, 0xF0, 0x10, 0xF0,	// 5
				0xF0, 0x80, 0xF0, 0x90, 0xF0,	// 6
				0xF0, 0x10, 0x20, 0x40, 0x40,	// 7
				0xF0, 0x90, 0xF0, 0x90, 0xF0,	// 8
				0xF0, 0x90, 0xF0, 0x10, 0xF0,	// 9
				0xF0, 0x90, 0xF0, 0x90, 0x90,	// A
				0xE0, 0x90, 0xE0, 0x90, 0xE0,	// B
				0xF0, 0x80, 0x80, 0x80, 0xF0,	// C
				0xE0, 0x90, 0x90, 0x90, 0xE0,	// D
				0xF0, 0x80, 0xF0, 0x80, 0xF0,	// E
				0xF0, 0x80, 0xF0, 0x80, 0x80	// F
			);
	for (var i=0;i<aFont.length;i++)
		this._aRAM[i] = aFont[i];

}



Chip8Emu.prototype._onKeyDown = function(e)
{
	if (this._bPaused) {
		return;
	}
	//if (this._bWaiting) {
	//	return;
	//}
	if (this._bKeyIsDown) {
		return;
	}
	var aKeys = this._aKeys;
	var iKeyCode = e.keyCode;

	for (var a=1;a<=16;a++) {
		if (iKeyCode == aKeys[a]) {
			this._iKeyPressed = a;
			break;
		}
	}
	this._bKeyIsDown = true;
}

Chip8Emu.prototype._onKeyUp = function(e) {
	this._iKeyPressed = 0;
	this._bKeyIsDown = false;
}


Chip8Emu.prototype._startCPU = function()
{
	if (this._bRunning) return;

	this.totalTime = new Date().getTime();

	this._bRunning = true;
	this._runTimer();
	var me = this;
	setTimeout(
		function() {
			me._runCPU();
		}, 1
	);
}

Chip8Emu.prototype._runTimer = function()
{
	if (!(this._bPaused || this._bWaiting)) {
		if (this._DT > 0) this._DT--;
		if (this._ST > 0) this._ST--;
	}
	var me = this;
	setTimeout(
		function() {
			me._runTimer();
		}, 17
	);
}

Chip8Emu.prototype._runCPU = function()
{
	if (this._bDebug) {
		if (!(this._bPaused || this._bWaiting)) 
			this._process();
	} else {
		var p = true;
		for (var i=0;i<10 && p;i++) {
			if (!(this._bPaused || this._bWaiting)) {
				p = this._process();
			}
		}
	}
	var me = this;
	setTimeout(
		function() {
			me._runCPU();
		}, 1
	);
}

Chip8Emu.prototype.setPause = function(bPause)
{
	this._bPaused = bPause;
}
Chip8Emu.prototype.getPause = function()
{
	return this._bPaused;
}

Chip8Emu.prototype.togglePause = function()
{
	if (this._bPaused)
		this._bPaused = false;
	else
		this._bPaused = true;
}


Chip8Emu.prototype.loadROM = function(aBytes)
{
	this.reset();

	for (var a=0;a<aBytes.length;a++)
		this._aRAM[0x200 + a] = aBytes[a];


	this._startCPU();
}

Chip8Emu.prototype._process = function()
{
	this._iOP1 = (this._aRAM[this._PC] & 0xF0) / 16;
	this._iOP2 = this._aRAM[this._PC] & 0xF;
	this._iOP3 = (this._aRAM[this._PC + 1] & 0xF0) / 16;
	this._iOP4 = this._aRAM[this._PC + 1] & 0xF;


	if (this._bDebug) wrtdbg("[" + hex(this._PC) + "] " + hex(this._aRAM[this._PC]) + " " + hex(this._aRAM[this._PC+1]) + " : " + hex(this._iOP1) + " " + hex(this._iOP2) + " " + hex(this._iOP3) + " " + hex(this._iOP4));

	this._PC += 2;

	switch (this._iOP1) {
		case 0x0 :
			switch(this._iOP3) {
				case 0xC :
					return this._opScrollDown();
				case 0xE :
					switch (this._iOP4) {
						case 0x0: 
							return this._opCLS();
						case 0xE:
							return this._opReturn();
					}
					break;
				case 0xF :
					switch (this._iOP4) {
						case 0xB :
							return this._opScrollRight();
						case 0xC :
							return this._opScrollLeft();
						case 0xE :
							return this._opSetLow();
						case 0xF :
							return this._opSetHigh();
					}
					break;
			}
			break;
		case 0x1 :
			return this._opJump();
		case 0x2 :
			return this._opCall();
		case 0x3 :
			return this._opSkipIfEqual();
		case 0x4 :
			return this._opSkipIfNotEqual();
		case 0x5 :
			return this._opSkipIfRegEqual();
		case 0x6 :
			return this._opLoadValue();
		case 0x7 :
			return this._opAddValue();
		case 0x8 :
			switch (this._iOP4) {
				case 0x0 :
					return this._opLoadReg();
				case 0x1 :
					return this._opOrReg();
				case 0x2 :
					return this._opAndReg();
				case 0x3 :
					return this._opXorReg();
				case 0x4 :
					return this._opAddReg();
				case 0x5 :
					return this._opSubReg();
				case 0x6 :
					return this._opShiftRight();
				case 0x7 :
					return this._opSubRegReverse();
				case 0xE :
					return this._opShiftLeft();
			}
			break;
		case 0x9 :
			return this._opSkipIfRegNotEqual();
		case 0xA :
			return this._opLoadValueIndex();
		case 0xB :
			return this._opJumpPlusReg0();
		case 0xC :
			return this._opRandom();
		case 0xD :
			return this._opDrawSprite();
		case 0xE :
			switch (this._iOP3) {
				case 0x9 :
					return this._opSkipIfKeyPressed();
				case 0xA :
					return this._opSkipIfNotKeyPressed();
			}
			break;
		case 0xF :
			switch (this._iOP3) {
				case 0x0 :
					switch (this._iOP4) {
						case 0x7 : 
							return this._opGetDelayTimer();
						case 0xA : 
							return this._opWaitForKeyPress();
					}
					break;
				case 0x1 :
					switch (this._iOP4) {
						case 0x5 :
							return this._opSetDelayTimer();
						case 0x8 :
							return this._opSetSoundTimer();
						case 0xE :
							return this._opAddRegToIndex();
					}
					break;
				case 0x2 :
					return this._opSetIndexToSprite();
				case 0x3 :
					return this._opStoreBCD();
				case 0x5 :
					return this._opStoreRegsAtIndex();
				case 0x6 :
					return this._opLoadRegsFromIndex();
			}
			break;
	}
}


Chip8Emu.prototype._opCLS = function()
{
	this._oScreen.clear();
	return true;
}

Chip8Emu.prototype._opJump = function()
{
	this._PC = (this._iOP2 << 8) + (this._iOP3 << 4) + this._iOP4;
	return true;
}

Chip8Emu.prototype._opCall = function() {
	this._aStack[this._SP] = this._PC;
	this._SP++;
	this._PC = (this._iOP2 << 8) + (this._iOP3 << 4) + this._iOP4;
	return true;
}

Chip8Emu.prototype._opReturn = function() {
	this._PC = this._aStack[--this._SP];
	return true;
}

Chip8Emu.prototype._opSkipIfEqual = function() {
	if (this._V[this._iOP2] == ((this._iOP3 << 4) + this._iOP4))
		this._PC += 2;
	return true;
}

Chip8Emu.prototype._opSetLow = function() {
	this._oScreen.setSize(64, 32, 4);
	this._bHigh = false;
	return true;
}

Chip8Emu.prototype._opSetHigh = function() {

	alert("SChip-8 is not supported, high mode won't work!");

	this._oScreen.setSize(128, 64, 2);
	this._bHigh = true;
	return true;
}

Chip8Emu.prototype._opSkipIfNotEqual = function() {
	if (this._V[this._iOP2] != ((this._iOP3 << 4) + this._iOP4))
		this._PC += 2;
	return true;
}

Chip8Emu.prototype._opSkipIfRegEqual = function() {
	if (this._V[this._iOP2] == this._V[this._iOP3])
		this._PC += 2;
	return true;
}

Chip8Emu.prototype._opSkipIfRegNotEqual = function() {
	if (this._V[this._iOP2] != this._V[this._iOP3])
		this._PC += 2;
	return true;
}

Chip8Emu.prototype._opLoadValue = function() {
	this._V[this._iOP2] = ((this._iOP3 << 4) + this._iOP4);
	return true;
}

Chip8Emu.prototype._opAddValue = function() {
	var val = (this._V[this._iOP2] + ((this._iOP3 << 4) + this._iOP4));
	if (val > 255) val -= 256;
	this._V[this._iOP2] =  val;
	return true;
}

Chip8Emu.prototype._opLoadReg = function() {
	this._V[this._iOP2] = this._V[this._iOP3];
	return true;
}

Chip8Emu.prototype._opOrReg = function() {
	this._V[this._iOP2] = this._V[this._iOP2] | this._V[this._iOP3];
	return true;
}

Chip8Emu.prototype._opAndReg = function() {
	this._V[this._iOP2] = this._V[this._iOP2] & this._V[this._iOP3];
	return true;
}

Chip8Emu.prototype._opXorReg = function() {
	this._V[this._iOP2] = this._V[this._iOP2] ^ this._V[this._iOP3];
	return true;
}

Chip8Emu.prototype._opAddReg = function() {
	var val = this._V[this._iOP2] + this._V[this._iOP3];
	if (val > 255) {
		val -= 256;
		this._V[0xF] = 1;
	} else this._V[0xF] = 0;

	this._V[this._iOP2] = val;
	return true;
}

Chip8Emu.prototype._opSubReg = function() {
	var val = this._V[this._iOP2] - this._V[this._iOP3];
	this._V[0xF] = val > 0 ? 1 : 0;
	if (val < 0) val += + 256;
	this._V[this._iOP2] = val;
	return true;
}

Chip8Emu.prototype._opShiftRight = function() {
	if (this._V[this._iOP2] & 1)
		this._V[0xF] = 1;
	else
		this._V[0xF] = 0;
	this._V[this._iOP2] = this._V[this._iOP2] / 2;
	return true;
}

Chip8Emu.prototype._opSubRegReverse = function() {
	var val = this._V[this._iOP3] - this._V[this._iOP2];
	this._V[0xF] = val > 0 ? 1 : 0;
	if (val < 0) val += 256;
	this._V[this._iOP2] = val;
	return true;
}

Chip8Emu.prototype._opShiftLeft = function() {
	if (this._V[this._iOP2] & 128)
		this._V[0xF] = 1;
	else
		this._V[0xF] = 0;
	var val = this._V[this._iOP2] * 2;
	if (val > 255) val -= 256;
	this._V[this._iOP2] = val;
	return true;
}

Chip8Emu.prototype._opLoadValueIndex = function() {
	this._I = (this._iOP2 << 8) + (this._iOP3 << 4) + this._iOP4;
	return true;
}

Chip8Emu.prototype._opJumpPlusReg0 = function() {
	this._PC = this._V[0] + (this._iOP2 << 8) + (this._iOP3 << 4) + this._iOP4;
	return true;
}

Chip8Emu.prototype._opRandom = function() {
	this._V[this._iOP2] = parseInt(Math.random() << 8) & ((this._iOP3 << 4) + this._iOP4);
	return true;
}

Chip8Emu.prototype._opSkipIfKeyPressed = function() {
	if (this._iKeyPressed == this._V[this._iOP2])
		this._PC += 2;
	return false;
}

Chip8Emu.prototype._opSkipIfNotKeyPressed = function() {
	if (this._iKeyPressed != this._V[this._iOP2])
		this._PC += 2;
	return false;
}

Chip8Emu.prototype._opSetDelayTimer = function() {
	this._DT = this._V[this._iOP2];
	return true;
}

Chip8Emu.prototype._opGetDelayTimer = function() {
	this._V[this._iOP2] = this._DT;
	return true;
}

Chip8Emu.prototype._opSetSoundTimer = function() {
	this._ST = this._V[this._iOP2];
	return true;
}

Chip8Emu.prototype._opAddRegToIndex = function() {
	this._I += this._V[this._iOP2];
	return true;
}

Chip8Emu.prototype._opSetIndexToSprite = function() {
	this._I = ((this._V[this._iOP2] & 0xf) * 5);
	return true;
}

Chip8Emu.prototype._opStoreBCD = function() {
	var num = this._V[this._iOP2];
	for (var i=3;i>=1;i--) {
		this._aRAM[this._I + (i - 1)] = num % 10;
		num = num / 10;
	}
	return true;
}

Chip8Emu.prototype._opStoreRegsAtIndex = function() {
	for (var i=0;i<=this._iOP2;i++)
		this._aRAM[this._I + i] = this._V[i];
	return true;
}

Chip8Emu.prototype._opLoadRegsFromIndex = function() {
	for (var i=0;i<=this._iOP2;i++)
		this._V[i] = this._aRAM[this._I + i];
	return true;
}

Chip8Emu.prototype._opDrawSprite = function() {
	var V = this._V;
	V[0xF] = 0;
	var scr = this._oScreen;
	if (!this._bHigh) {
		var x = V[this._iOP2];
		var y = V[this._iOP3];
		var OP4 = this._iOP4;
		for (var j=0;j<OP4;j++) {
			var spr = this._aRAM[this._I + j];

			for (var i=0;i<8;i++) {
				if ((spr & 0x80) > 0) {
					if (scr.drawPixel(x+i,y+j) == 1) V[0xF] = 1;
				}
				spr <<= 1;
			}

		}
	} else {
		wrtdbg("SChip not supported!");
	}
	return true;
}




Chip8Emu.prototype._opScrollDown = function()
{
	wrtdbg("Scrolling not supported");
	return true;
}

Chip8Emu.prototype._opScrollRight = function()
{
	wrtdbg("Scrolling not supported");
	return true;

}


Chip8Emu.prototype._opWaitForKeyPress = function()
{
	this._iKeyPressed = 0;
	this._bWaiting = true;
	this._checkKeyPress();
	return true;
}

Chip8Emu.prototype._checkKeyPress = function() {
	if (this._iKeyPressed == 0) {
		var me = this;
		setTimeout(
			function() {
				me._checkKeyPress();
			}, 5
		);
	} else {
		this._V[this._iOP2] = this._iKeyPressed;
		this._bWaiting = false;
	}
}


addEvent(window, "load", init);


})();
