(function () {

var HTE_VERSION = "HTE 1.0";

var HTE_ACTION = {
    HTE_HOST_ACTION_CONNECTED               : 1,
    HTE_HOST_ACTION_UPDATE                  : 2,
    HTE_HOST_ACTION_SET_CURSOR              : 3,
    HTE_HOST_ACTION_GET_ENTRY               : 4,
    HTE_HOST_ACTION_GET_YESNO               : 5,
    HTE_HOST_ACTION_SHOW_FATAL_ERROR        : 6,
    HTE_CLIENT_ACTION_REFRESH               : 4000,
    HTE_CLIENT_ACTION_CONNECT_HOST          : 4001,
    HTE_CLIENT_ACTION_ONKEY                 : 4002,
    HTE_CLIENT_ACTION_GET_ENTRY_RESPONSE    : 4003,
    HTE_CLIENT_ACTION_GET_YESNO_RESPONSE    : 4004,
    HTE_CLIENT_ACTION_ACK_FATAL_ERROR       : 4005
};

var ANSI_ATTR = {
	NONE		:	0x00000000,
	BOLD		:	0x00100000,
	UNDERLINE	:	0x00200000,
	BLINK		:	0x00400000,
	REVERSE		:	0x00800000
};

var TEColors = [
	"#000000", // black
	"#000080", // lt-blue
	"#008000", // lt-green
	"#008080", // lt-cyan
	"#800000", // lt-red
	"#800080", // lt-magenta
	"#808080", // gold
	"#c1c1c1", // lt-gray
	"#808080", // gray
	"#0000ff", // blue
	"#00ff00", // green
	"#00ffff", // cyan
	"#ff0000", // red
	"#ff00ff", // magenta
	"#ffff00", // yellow
	"#ffffff"  // white
];

var TE_KeyCode = {
	F1: 63236,
	SF1: 61769,
	CF1: 61789,
	CSF1: 61829,
};

var IB_KeyCode = {
	F1: 112,
	F12: 123
};

var IB_TO_TE = {
	33: 0xf128, 34: 0xf127, 35: 0xf72b, 36: 0xf729, // pgup, pgdown, end, home
	37: 0xf117, 38: 0xf114, 39: 0xf116, 40: 0xf115, // left, up, right, down 
	9: 9, 46: 61741, // tab, forward delete
	96: 61721, 97: 61722, 98: 61723, 99: 61724, 100: 61725, // KP0-KP9
	101: 61726, 102: 61727, 103: 61728, 104: 61729, 105: 61730,
	106: 61754, 107: 61755, 109: 61731, 110: 61733, 111: 61756 // star, plus, dash, dot, slash
};

var IB_TO_TE_C = {
	9: 61766, 33: 61746, 34: 61747, 35: 61739, 36: 61737, // tab, pgup, pgdown, end, home
	37: 61743, 38: 61760, 39: 61742, 40: 61761, // left, up, right, down
	111: 61767, 106: 61753, 109: 61751, 107: 61752 // slash, star, dash, plus
};

var IB_TO_TE_S = {
	9: 61745 // tab
};

window.HTEClientConsole = HTEClientConsole;

function HTEClientConsole (id) {
	var teConsole = this;
	var cols = 80;
	var lines = 25;
	var factor = 4;
	var emulate;
	var cellSizeX = 8 * factor;
	var cellSizeY = 20 * factor;
	var fontSize = 12 * factor;
	var cursorX = 0;
	var cursorY = 0;
	var cursorColor = "#00ff00";
	var cursorBlinkState = true;
	var cursorVisible = true;
	var cursorRestoreImage;
	var cursorTimer = -1;
	var blinkTimer = -1;
	var showBlinkCharacters = false;
	var blinkCharacters = {};
	var handleKeyEvents = true;
	var element = document.getElementById(id);

	openTeChannel();

	function initConsole() {
		teConsole.getConfigValues(["emulate", "lines", "col", "CursorColor"], function (obj) {

			emulate = parseInt(obj.emulate, 10);

			if (emulate >= 24 && emulate <= 27) {
				switch (emulate) {
					case 24:
						cols = 80; 
						lines = 24;
						break;
					case 25:
						cols = 80;
						lines = 32;
						break;
					case 26:
						cols = 80;
						lines = 43;
						break;
					case 27:
						cols = 132;
						lines = 27;
						break;
				}
			}
			else {
				lines = parseInt(obj.lines, 10);
				cols = parseInt(obj.col, 10);
			}

			cursorColor = TEColors[obj.CursorColor];

			WebFont.load({
				custom: {
					families: ["lucon"],
					urls: ["/console.css"]
				},
				fontactive: function (fontFamily, fvd) {
					if (fontFamily == "lucon") {

						initScreen();
					}
				}
			});
		});
	}

	function getCanvasCtx() {
		if (!getCanvasCtx.ctx)
			getCanvasCtx.ctx = element.getContext("2d");

		return getCanvasCtx.ctx;
	}

	function initScreen() {
		ctx = getCanvasCtx();

		element.width = cellSizeX * cols;
		element.height = cellSizeY * lines;

		ctx.font = fontSize + "px Lucon";
		ctx.fillStyle = '#000000';
		ctx.fillRect(0, 0, element.width, element.height);

		if (cursorTimer == -1)
			cursorTimer = setInterval(toggleCursor, 400);

		if (blinkTimer == -1)
			blinkTimer = setInterval(toggleBlinkCharacters, 400);

		hteRequestRefresh();

		closeDialog("#entryDialog");

		handleKeyEvents = true;
	}

	function drawChar(attrs, line, col, showForeground) {
		var ctx = getCanvasCtx();

		var bg = (attrs[0] >> 28) & 0x0f;
		var fg = (attrs[0] >> 24) & 0x0f;
		var ch = String.fromCharCode(attrs[1] & ~(1 << 31));

		var t;

		if (attrs[0] & ANSI_ATTR.REVERSE) {
			t = bg;
			bg = fg;
			fg = t;	
		}

		if (attrs[0] & ANSI_ATTR.BLINK) {
			if (!blinkCharacters[line])
				blinkCharacters[line] = {};
			blinkCharacters[line][col] = attrs;
		}
		else if (blinkCharacters[line] && blinkCharacters[line][col])
			delete(blinkCharacters[line][col]);

		var x = col * cellSizeX;
		var y = line * cellSizeY;

		ctx.fillStyle = TEColors[bg];
		ctx.fillRect(x, y, cellSizeX, cellSizeY);

		if (showForeground) {
			ctx.fillStyle = TEColors[fg];
			ctx.fillText(ch, x, y + fontSize);
		}

		if (attrs[0] & ANSI_ATTR.UNDERLINE) {
			ctx.beginPath();
			ctx.strokeStyle = ctx.fillStyle;
			ctx.moveTo(x, y + cellSizeY);
			ctx.lineTo(x + cellSizeX, y + cellSizeY);
			ctx.stroke();
		}
	}

	function showCursor() {
		if (!cursorVisible || !cursorBlinkState)
			return false;

		var ctx = getCanvasCtx();

		cursorRestoreImage = ctx.getImageData(cursorX, cursorY, cellSizeX, cellSizeY);

		ctx.fillStyle = cursorColor;
		ctx.fillRect(cursorX, cursorY, cellSizeX, cellSizeY);

		cursorBlinkState = false;

		return true;
	}

	function hideCursor() {
		if (cursorBlinkState)
			return false;

		var ctx = getCanvasCtx();

		ctx.putImageData(cursorRestoreImage, cursorX, cursorY);

		cursorBlinkState = true;

		return true;
	}

	function setCursorPosition(x, y) {
		hideCursor();
		cursorX = x * cellSizeX;
		cursorY = y * cellSizeY;
		showCursor();
	}

	function toggleCursor() {
		if (cursorBlinkState)
			showCursor();
		else
			hideCursor();
	}

	function toggleBlinkCharacters() {
		for (var y in blinkCharacters)
			for (var x in blinkCharacters[y])
				drawChar(blinkCharacters[y][x], y, x, showBlinkCharacters);

		showBlinkCharacters = !showBlinkCharacters;
	}

	function getASCIIStringFromInt8Array(bytes, len) {
		var str = "";

		for (var i = 0; i < len; i++) {
			str += String.fromCharCode(bytes[i]);
		}

		return str;
	}

	function getBodyOffset(bytes) {
		var i = 2;

		for (;;) {
			if (bytes[i - 2] == 0x0A && bytes[i - 1] == 0x0A) {
				for (;;) {
					if (bytes[i++] == 0x0A) {
						return i;
					}
				}
			}

			i++;
		}
	}

	function getArrayBufferOffset(buf, offset) {
		if (buf.slice)
			return buf.slice(offset);
		else {
			var newLen = buf.byteLength - offset;
			var newBuf = new ArrayBuffer(newLen);

			var src = new Int8Array(buf);
			var dst = new Int8Array(newBuf);

			for (var i = 0; i < newLen; i++) {
				dst[i] = src[i + offset];
			}

			return newBuf;
		}
	}

	function getHeaderFields(bytes) {
		var i = getBodyOffset(bytes);

		str = getASCIIStringFromInt8Array(bytes, i);

		return str.split("\n");
	}

	/* Send a message over the HTE channel.
	 *
	 * action - A numeric code that corresponds to an HTE action.
	 * fields - An optional array of strings that contains information about the message.
	 * data - An optional ArrayBuffer object that contains binary data.
	 *
	 * While the fields and data arguments are optional as per the communication
	 * protocol, each action has its own requirements. */
	this.hteSendMessage = function (action, fields, data) {
		var data;

		if (!_ws)
			return;

		if (!fields)
			fields = [];

		if (!data)
			data = new ArrayBuffer(0);

		if (typeof action !== "number") {
			console.error("Error (hteSendMessage): Argument [action] must be of type [number].");
			return;
		}

		if (typeof fields !== "object" || !(fields instanceof Array)) {
			console.error("Error (hteSendMessage): Argument [fields] must be an array.");
			return;
		}

		if (typeof data !== "object" || !(data instanceof ArrayBuffer)) {
			console.error("Error (hteSendMessage): Argument [data] must be an object of type [ArrayBuffer].");
			return;
		}

		fields.unshift(action);
		fields.unshift(HTE_VERSION);
		fields.push("\n");

		var hdr = fields.map(String).join("\n") + String(data ? data.byteLength : 0) + "\n";

		_ws.send(ArrayBuffer.combine(hdr.toUTF8(), data));
	};

	function hteRequestRefresh() {
		teConsole.hteSendMessage(HTE_ACTION.HTE_CLIENT_ACTION_REFRESH);
	}

	function hteSendKey(keyCode) {
		var data = new ArrayBuffer(4);
		var ptr = new Int32Array(data);

		ptr[0] = keyCode;

		teConsole.hteSendMessage(HTE_ACTION.HTE_CLIENT_ACTION_ONKEY, [], data);
	}

	function openTeChannel() {
		var protocol = (window.location.protocol === "https:") ? "wss:" : "ws:";
		var url = protocol + "//" + window.location.hostname + ":" + window.location.port + "/get_te_channel";

		_ws = new WebSocket(url, "teCtrl");

		_ws.onopen = function () {
			$(document).on("keydown", function (e) {
				if (!handleKeyEvents)
					return;

				var keyCode = e.keyCode;
				var mappedKey;

				if (keyCode == 8 || // bs
					keyCode == 27) { // esc
					hteSendKey(keyCode);
					e.preventDefault();
				}
				else if (keyCode >= IB_KeyCode.F1 && keyCode <= IB_KeyCode.F12) {
					var fKey = keyCode - IB_KeyCode.F1;
					var offset;
					if (e.shiftKey) {
						if (e.ctrlKey)
							offset = TE_KeyCode.CSF1;
						else 
							offset = TE_KeyCode.SF1;
					}
					else if (e.ctrlKey)
						offset = TE_KeyCode.CF1;
					else
						offset = TE_KeyCode.F1;	

					hteSendKey(offset + fKey);
				}
				else {
					if (e.ctrlKey && (keyCode >= '@'.charCodeAt(0) && keyCode <= '_'.charCodeAt(0))) {
						mappedKey = keyCode & ~0x60;
					}
					else {
						if (e.shiftKey)
							mappedKey = IB_TO_TE_S[keyCode];	
						else if (e.ctrlKey)
							mappedKey = IB_TO_TE_C[keyCode];
						else
							mappedKey = IB_TO_TE[keyCode];
					}

					if (mappedKey) {
						hteSendKey(mappedKey);
						e.preventDefault();
					}
				}
			});

			$(document).on("keypress", function (e) {
				if (!handleKeyEvents)
					return;

				hteSendKey(e.which || e.keyCode);

				e.preventDefault();
			});
		};

		_ws.onmessage = function (msg) {
			var reader = new FileReader();

			reader.addEventListener("loadend", function () {
				var bytes = new Int8Array(reader.result);
				var fields = getHeaderFields(bytes);
				var actionCode = parseInt(fields[1], 10);

				if (actionCode === HTE_ACTION.HTE_HOST_ACTION_CONNECTED) {
					var isConnected = parseInt(fields[2], 10);

					if (isConnected) {
						initConsole();	
					}
				}
				else if (actionCode === HTE_ACTION.HTE_HOST_ACTION_SET_CURSOR) {
					var m = fields[2].match(/^([0-9]+):([0-9]+)$/);

					var x = parseInt(m[1], 10);
					var y = parseInt(m[2], 10);

					cursorVisible = parseInt(fields[3], 10);

					setCursorPosition(x, y);
				}
				else if (actionCode === HTE_ACTION.HTE_HOST_ACTION_UPDATE) {
					var vscreen = new Int32Array(getArrayBufferOffset(reader.result, getBodyOffset(bytes)));
                                    
					var m = fields[2].match(/^([0-9]+):([0-9]+):([0-9]+):([0-9]+)$/);

					var left = parseInt(m[1], 10);
					var bottom = parseInt(m[2], 10);
					var right = parseInt(m[3], 10);
					var top = parseInt(m[4], 10);
					var x, y;
					var pos = 0;

					hideCursor();

					for (y = top; y <= bottom; y++) {
						for (x = left; x <= right; x++) {
							drawChar([vscreen[pos], vscreen[pos + 1]], y, x, true);

							pos += 2;
						}
					}
				}
				else if (actionCode == HTE_ACTION.HTE_HOST_ACTION_GET_ENTRY) {
					handleKeyEvents = false;

					var isPassword = parseInt(fields[4], 10);

					var title = decodeURIComponent(fields[2]);
					var msg = decodeURIComponent(fields[3]);

					getEntry(title, msg, isPassword, function (status, msg) {

						teConsole.hteSendMessage(HTE_ACTION.HTE_CLIENT_ACTION_GET_ENTRY_RESPONSE, [
							encodeURIComponent(msg),
							status
						]);
						
						handleKeyEvents = true;	
					});
				}
				else if (actionCode == HTE_ACTION.HTE_HOST_ACTION_GET_YESNO) {
					handleKeyEvents = false;

					var title = decodeURIComponent(fields[2]);
					var msg = decodeURIComponent(fields[3]);

					getYesNoQuestionResponse(title, msg, function (status) {

						teConsole.hteSendMessage(HTE_ACTION.HTE_CLIENT_ACTION_GET_YESNO_RESPONSE, [ status ]);

						handleKeyEvents = true;
					});
				}
				else if (actionCode == HTE_ACTION.HTE_HOST_ACTION_SHOW_FATAL_ERROR) {
					handleKeyEvents = false;

					var msg = decodeURIComponent(fields[3]);

					showFatalError(msg, function () {
						teConsole.hteSendMessage(HTE_ACTION.HTE_CLIENT_ACTION_ACK_FATAL_ERROR);

						handleKeyEvents = true;
					});
				}
				else {
				}
			});

			reader.readAsArrayBuffer(msg.data);
		};

		_ws.onclose = function (e) {
			if (!e.wasClean) {
				showFatalError("Connection closed by host.");
			}

			_ws = null;
		};

		_ws.onerror = function (err) {
            console.log(err);
		};
	}
};

HTEClientConsole.prototype.getConfigValues = function (keys, cb) {
	if (!keys || !keys.length) {
		alert("Must specify keys to fetch from host.");
		return;
	}

	if (!cb) {
		alert("Must provide callback function.");
		return;
	}

	$.ajax({
		url: "/get_config_values",
		type: "POST",
		contentType: "application/json",
		dataType: "json",
		data: JSON.stringify(keys)
	})
	.done(function (res) {
		cb(res.content);
	});
};

function utf8_size(code)
{
	var bits = 31;

	if (code <= 127)
		return 1;

	while (!(code & (1 << bits))) {
		bits--;
	}

	return Math.ceil(bits / 5);
}

function appendUTF8ForCode(buffer, code)
{
	var n_bytes;
	var bytes;
	var bitsLeft;
	var i, j;

	if (!buffer || !(buffer instanceof Array)) {
		alert("Error (appendUTF8ForCode): Argument [buffer] must be of type [array].");
		return;
	}

	if (code <= 127) {
		buffer.push(code);
		return;
	}

	n_bytes = utf8_size(code);
	bytes = new Array(n_bytes);

	for (i = n_bytes - 1; i >= 0; i--) {
		var byte = 0;

		bitsLeft = 7;

		if (i == 0) {
			for (j = 0; j < n_bytes; j++) {
				byte |= (1 << (7 - j));
				bitsLeft--;
			}

			bitsLeft--;
		}
		else {
			byte |= 0x80;
			bitsLeft = 5;
		}

		j = 0;

		if (code > 0) { // still have bits left
			while (bitsLeft >= 0) { // remaining bits in this byte
				if (code & 1) {
					byte |= (1 << j);
				}

				j++;
				bitsLeft--;
				code >>= 1;
			}
		}

		bytes[i] = byte;
	}

	for (i = 0; i < bytes.length; i++)
		buffer.push(bytes[i]);
}

String.prototype.toUTF8 = function () {
	var i;
	var code;
	var bytes = [];
	var buf;
	var u8;

	for (i = 0; i < this.length; i++) {
		code = this.charCodeAt(i);

		if (code >= 0xD7FF && code <= 0xE000) {
			var code2 = this.charCodeAt(++i);

			code -= 0xD800;
			code2 -= 0xDC00;

			code <<= 10;

			code = 0x10000 + (code | code2);
		}

		appendUTF8ForCode(bytes, code);
	}

	buf = new ArrayBuffer(bytes.length);
	v8 = new Uint8Array(buf);

	for (i = 0; i < bytes.length; i++) {
		v8[i] = bytes[i];
	}

	return buf;
};

ArrayBuffer.combine = function (buf1, buf2) {
	var newBuf = new ArrayBuffer(buf1.byteLength + buf2.byteLength);

	var dst = new Int8Array(newBuf); 
	var src;

	var i;

	src = new Int8Array(buf1);

	for (i = 0; i < buf1.byteLength; i++) {
		dst[i] = src[i];
	}

	src = new Int8Array(buf2);

	for (i = 0; i < buf2.byteLength; i++) {
		dst[i + buf1.byteLength] = src[i];
	}

	return dst;
};

})();
