/* spiffy links */

// http://www.amazon.com/gp/product/offer-listing/0394756827/ref=dp_pb_a//102-3710615-5652939?condition=all
// http://cgibin.erols.com/ziring/cgi-bin/cep/cep.pl?_key=FLooP
// http://cgibin.erols.com/ziring/cgi-bin/cep/cep.pl?_key=BLooP
// http://www.pdc.kth.se/~jas/retro/retromuseum.html
// http://c2.com/cgi/wiki?BloopFloopAndGloop

/* parsing variables */

var BlooP = null;

var parseCode = null;
var codeLines = null;
var ns = null;
var val = null;

var code = "";
var computedCode = "";

var formals = [];
var blocks = [];

var LOOP_UNUSED = null;
var LOOP_USED = 1;
var LOOP_NEW_ABORTABLE = 2;
var LOOP_ABORTABLE = 3;
var LOOP_ABORTED = 4;

/* html elements */

var codeInput = null;
var compilerOutput = null;
var jsOutput = null;
var programOutput = null;

function setHtmlObjs() {
	codeInput = document.getElementById("codeInput");
	compilerOutput = document.getElementById("compilerOutput");
	jsOutput = document.getElementById("jsOutput");
	programOutput = document.getElementById("programOutput");
}

function compile() {
	setHtmlObjs();

	// check which language to compile in
	if (document.getElementById("bloopOption").checked == false 
	    && document.getElementById("floopOption").checked == false) {
		alert("Please select a language to compile in, either BlooP or FlooP.");
		return false;
	} else if (document.getElementById("floopOption").checked == true) {
		BlooP = "FlooP";
	} else {
		BlooP = "BlooP";
	}

	// clear existing variables
	parseCode = codeLines = computedCode = code = "";

	// setup the parseCode variable
	parseCode = codeInput.value;
	if (parseCode == null) return false;
	parseCode = parseCode.toUpperCase();
	parseCode = parseCode.replace(/\?/g, "q");
	parseCode = parseCode.replace(/-/g, "_");

	// get number of lines in code
	codeLines = parseCode.split("\n").length + 1;

	// clear the warning box and the outputted source
	removeAllChildren(jsOutput);
	removeAllChildren(compilerOutput);

	// parse all functions
	while (!parseCode.match(/^$/)) {
		parse();
		computedCode += code;
	}

	// output the compiled javascript code
	jsOutput.value = computedCode;

	// if not enabled, enable the "run" button
	document.getElementById("runCode").disabled = false;

	// finally,
	// tell the user the compilation is completed
	notice("Compilation Completed.");
}

function executeCode() {
	// compile and run existing code
	compile();
	eval(computedCode);

	// separate different program's output
	programOutput.value += "\n";
}

function run() {
	// run any generated code
	eval(computedCode);

	// separate different program's output
	programOutput.value += "\n";
}

function removeAllChildren(n) {	// removes all child nodes from node n
	var children = n.childNodes.length;
	for (i = 0; i < children; i++) { n.removeChild(n.childNodes[0]); }
}

/* compiler output functions */

function error(warning) {
	var warnBox = document.createElement("div");
	warnHeader = document.createElement("strong");
	warnHeader.appendChild(document.createTextNode("Error: Line " + (codeLines - parseCode.split("\n").length) + ":"));
	warnHeader.appendChild(document.createElement("br"));
	warnBox.appendChild(warnHeader);
	warnBox.appendChild(document.createTextNode(warning));
	warnBox.style.color = "red";
	compilerOutput.appendChild(warnBox);
}

function warn(warning) {
	var warnBox = document.createElement("div");
	warnHeader = document.createElement("strong");
	warnHeader.appendChild(document.createTextNode("Warning: Line " + (codeLines - parseCode.split("\n").length) + ":"));
	warnHeader.appendChild(document.createElement("br"));
	warnBox.appendChild(warnHeader);
	warnBox.appendChild(document.createTextNode(warning));
	compilerOutput.appendChild(warnBox);
}

function notice(warning) {
	var warnBox = document.createElement("div");
	warnHeader = document.createElement("strong");
	warnHeader.appendChild(document.createTextNode("Notice: Line " + (codeLines - parseCode.split("\n").length) + ":"));
	warnHeader.appendChild(document.createElement("br"));
	warnBox.appendChild(warnHeader);
	warnBox.appendChild(document.createTextNode(warning));
	warnBox.style.color = "green";
	compilerOutput.appendChild(warnBox);
}

/* editing functions */

function copyItem(n) {
	n.select();
}

function clearItem(n) {
	if (n.tagName == "TEXTAREA")
		n.value = "";
	else
		removeAllChildren(n);
}

/*********************/
/* Parsing Functions */
/*********************/

function scan() {
	val = null;

	MATCH: 
	while (true) {
		var breakS =
			[/^''/,
			/^<=/,
			/^[+*!=<>(){}":;,.-]/,
			/^\[/,
			/^\]/,
			/^DEFINE\b/,
			/^PROCEDURE\b/,
			/^BLOCK\b/,
			/^LOOP\b/,
			/^AT\b/,
			/^MOST\b/,
			/^TIMES\b/,
			/^MU_LOOP\b/,
			/^CELL\b/,
			/^OUTPUT\b/,
			/^YES\b/,
			/^NO\b/,
			/^QUIT\b/,
			/^ABORT\b/,
			/^IF\b/,
			/^THEN\b/,
			/^AND\b/,
			/^OR\b/,
			/^PRINT\b/];

		if (parseCode.match(/^$/)) return false;

		if (parseCode.match(/^\/\*.*\*\//)) {
			parseCode = parseCode.replace(/^\/\*.*\*\//, "");
			continue MATCH;
		}

		if (parseCode.match(/^BLOCK\s+(\d+)\s*:\s*BEGIN/)) {
			ns = "BEGIN";
			val = parseCode.match(/^BLOCK\s+(\d+)\s*:\s*BEGIN/)[1];
			parseCode = parseCode.replace(/^BLOCK\s+(\d+)\s*:\s*BEGIN/, "");
			break MATCH;
		}

		if (parseCode.match(/^BLOCK\s+(\d+)\s*:\s*END/)) {
			ns = "END";
			val = parseCode.match(/^BLOCK\s+(\d+)\s*:\s*END/)[1];
			parseCode = parseCode.replace(/^BLOCK\s+(\d+)\s*:\s*END/, "");
			break MATCH;
		}

		for (var i=0, max=breakS.length; i<max; i++) {
			if (parseCode.match(breakS[i])) {
				ns = parseCode.match(breakS[i])[0];
				parseCode = parseCode.replace(breakS[i], "");
				break MATCH;
			}
		}

		if (parseCode.match(/^\s+/)) {
			parseCode = parseCode.replace(/^\s+/, "");
			continue MATCH;
		}

		if (parseCode.match(/^[A-Z]\w*/)) {
			ns = "ID";
			val = parseCode.match(/^[A-Z]\w*/)[0];
			parseCode = parseCode.replace(/^[A-Z]\w*/, "");
			break MATCH;
		}

		if (parseCode.match(/^\d+/)) {
			ns = "NUMBER";
			val = parseCode.match(/^\d+/)[0];
			parseCode = parseCode.replace(/^\d+/, "");
			break MATCH;
		}

		if (parseCode.match(/^'/)) {
			string();
			break MATCH;
		}

		invChar = parseCode.match(/^./);
		invChar = invChar[0];
		invChar = invChar.replace(/_/g, "-");
		invChar = invChar.replace(/q/g, "?");
		error("BlooP: invalid character " + invChar);
		parseCode = parseCode.replace(/^./, "");
		break MATCH;
	}
}

function string() {
	var result = "";

	while (true) {
		if (parseCode.search(/^'[^']*'/) == -1)
			break;

		result += parseCode.match(/^'[^']*'/)[0];
		parseCode = parseCode.replace(/^'[^']*'/, "");
	}

	ns = "STRING";
	val = result.substring(1, result.length - 1);
}

function parse() {
	code = "";
	formals = blocks = [];

	scan();
	if (ns == "DEFINE") {
		definition();
	} else {
		code += "printstring(";
		expression();
		if (code.match(/q/))
			code += " ? \"YES\" : \"NO\"";
		code += ");\n";
	}

	if (ns != ".") warn("excess junk at end of function");
}

function descan(locNs, locVal)  {
	if (locNs == "BEGIN" || locNs == "END")
		return "BLOCK " + locVal + ": " + locNs;
 	if (locVal == "")
		return locNs;
	return locNs + " " + locVal;
}

function need(needVar) {
	var oldval = val;

	if (needVar != ns) {
		warn("expected " + descan(needVar, val) +
			", got " + descan(ns, ""));
	}

	scan();
	return oldval;
}

function definition() {
	var name = "";

	scan();
	need("PROCEDURE");
	need("''");
	name = need("ID");
	code += "function " + name;
	name = name.replace(/q/, "?");
	name = name.replace(/_/g, "-");

	need("''");
	need("[");
	getformals();
	need("]");
	need(":");
	code +=  " {\n";
	code += "var cell = new Array();\n";
	code += "var output = 0;\n";
	statement();
	code += "return output;\n";
	code += "}\n\n";
}

function getformals() {
	formals = [];

	while (true) {
		formals.push(need("ID"));
		if (ns != ",") break;
		scan();
	}

	code += "(" + formals.join(", ") + ")";
	}

function statement() {
	if (ns == "BEGIN") { block(); return; }
	if (ns == "LOOP") { loop(); return; }
	if (ns == "MU_LOOP") { mu_loop(); return; }
	if (ns == "QUIT") { quit(); return; }
	if (ns == "ABORT") { abort(); return; }
	if (ns == "IF") { f_if(); return; }
	if (ns == "PRINT") { print(); return; }
	assign(); return;
}

function block() {
	var begin = val;
	var end = "";

	if (blocks[begin] == LOOP_UNUSED) {
		blocks[begin] = LOOP_USED;
		code += "BLOCK" + begin + ": {\n";
	} else if (blocks[begin] == LOOP_USED || blocks[begin] == LOOP_ABORTABLE) {
		warn("BLOCK " + begin + " appears twice");
	} else if (blocks[begin] == LOOP_NEW_ABORTABLE) {
		blocks[begin] = LOOP_ABORTABLE;
	}

	scan();
	while (ns != "END")
		statement();

	end = val;
	if (begin == end)
		notice("BLOCK " + begin + ": BEGIN matches with BLOCK " + end + ": END");

	scan();
	if (blocks[begin] != LOOP_ABORTABLE && blocks[begin] != LOOP_ABORTED)
		code += "}\n"

	if (ns == ";") scan();
}

function loop() {
	var atmost = "";

	scan();
	if (ns == "AT") {
		scan();
		need("MOST");
		atmost = 1;
	}

	if (atmost) code += "BLOCK#: ";
	code += "for (var counter = 0, ";
	code += "limit = ";
	expression();
	code += "; counter < limit; counter++) {\n";

	need("TIMES");
	need(":");
	if (atmost) {
		if (ns != "BEGIN")
			warn("LOOP AT MOST requires following BLOCK");

		code = code.replace(/#/, val);
		blocks[val] = LOOP_NEW_ABORTABLE;
	}

	statement();
	code += "}\n";
}

function mu_loop() {
	if (BlooP == "BlooP")
		error("MU-LOOP not supported -- use FlooP");

	var loopnum = 0;

	scan();
	need(":");

	if (ns != "BEGIN")
		warn("LOOP requires following BLOCK");

	loopnum = val;
	code += "BLOCK" + loopnum + ": while (true) {\n";
	blocks[loopnum] = LOOP_NEW_ABORTABLE;

	statement();
	code += "}\n";

	if (blocks[loopnum] != LOOP_ABORTED)
		warn("FlooP: MU-LOOP without ABORT LOOP may run forever");
}

function quit() {
	var blocknum = "";

	scan();
	need("BLOCK");
	blocknum = need("NUMBER");

	if (blocks[blocknum] == LOOP_UNUSED)
		error("QUIT BLOCK refers to non-existent BLOCK");

	code += "break BLOCK" + blocknum + ";\n";
	need(";");
}

function abort() {
	var loopnum = "";

	scan();
	need("LOOP");
	loopnum = need("NUMBER");

	if (blocks[loopnum] != LOOP_ABORTABLE && blocks[loopnum] != LOOP_ABORTED)
		error("ABORT LOOP on non-abortable loop or non-loop");

	code += "break BLOCK" + loopnum + ";\n";
	need(";");
	blocks[loopnum] = LOOP_ABORTED;
}

function f_if() {
	code += "if (";

	scan();
	if (ns == "{") {
		scan();
		while (true) {
			expression();
			if (ns == "AND") {
				code += " && ";
				scan();
			} else if (ns == "OR") {
				code += " || ";
				scan();
			} else {
				break;
			}
		}
		need("}");
	} else {
		expression();
	}

	code += ") {\n";
	need(",");
	need("THEN");
	need(":");
	statement();
	code += "}\n";
}

function print() {
	scan();
	need("[");
	code += "printstring(";

	while (true) {
		expression();
		if (ns != ",") break;
		scan();
		code += ", ";
	}

	need("]");
	code += ");\n";
	if (ns == ";") scan();
}

function assign() {
	if (ns == "OUTPUT") {
		code += "output";
		scan();
	} else if (ns == "CELL") {
		cell();
	} else {
		error("invalid syntax");
		while (ns != ";") scan();
		scan();
		return;
	}

	need("<=");
	code += " = ";
	expression();
	code += ";\n";
	if (ns == ";") scan();
}

function expression() {
	if (ns == "STRING") {
		val = val.replace("\\", "\\\\");
		val = val.replace("'", "\\'");
		code += "'" + val + "'";
		scan();
	} else if (ns == "YES") {
		code += "1";
		scan();
	} else if (ns == "NO") {
		code += "0";
		scan();
	} else {
		while (true) {
			term();
			if (ns == "=") {
				code += " == ";
			} else if (ns == "<") {
				code += " < ";
			} else if (ns == ">") {
				code += " > ";
			} else {
				break;
			}
			scan();
		}
	}
}

function term() {
	while (true) {
		factor();
		if (ns != "+") break;
		scan();
		code += " + ";
	}
}

function factor() {
	while (true) {
		primary();
		if (ns != "*") break;
		scan();
		code += " * ";
	}
}

function primary() {
	if (ns == "CELL") {
		cell();
	} else if (ns == "OUTPUT") {
		code += "output";
		scan();
	} else if (ns == "ID") {
		var id = val;
		scan();

		if (ns == "[") {
			scan();
			code += id + "(";
			while (true) {
				expression()
				if (ns != ",") break;
				scan();
				code += ", ";
			}
			need("]");
			code += ")";
			return;
		}

		for (var i=0, max=formals.length; i<max; i++) {
			if (formals[i] == id) break;
			if (i == max - 1) {
				id = id.replace(/q/, "?");
				id = id.replace(/_/, "-");
				warn("unknown variable " + id);
			}
		}

		code += id;
	} else if (ns == "NUMBER") {
		code += val;
		scan();
	} else {
		warn("unexpected " + descan(ns, val) + " found");
		scan();
	}
}

function cell() {
	scan();
	need("(");
	code += "cell[" + need("NUMBER") + "]";
	need(")");
}

/*********************/
/* Program Functions */
/*********************/

function printstring() {
	setHtmlObjs();

	var output = " > ";
	for (var i=0; i<arguments.length; i++)
		output += arguments[i];

	programOutput.value += output + "\n";
}
