/**
 **		All this stuff is copyright 2007 Jeremy Smith
 **		Please don't use this without asking me first (my first name at duckwizard dot com)
 **		Soon to be released to the public, so just be patient if you like it and want to use it.
 */

// make sure we have some stack functions in the Array prototype
if(typeof(Array.prototype.push) == "undefined")
{
	Array.push = function(value)
	{
		this[this.length] = value;
	}
}

if(typeof(Array.prototype.pop) == "undefined")
{
	Array.pop = function()
	{
		var newLength = this.length -1;
		var popped = this[newLength];
		
		delete this[newLength];
		this.length = newLength;
		return popped;
	}
}

if(typeof(Array.prototype.peek) == "undefined")
{
	Array.prototype.peek = function()
	{
		return this[this.length - 1];
	}
}


// State-aware lexer
function Lexer(tokens)
{
	this.tokens = tokens;	
	this.text = "";
	this.lastIndex = 0;

	this.init = function(text)
	{

		this.text = text;
		this.lastIndex = 0;
	}
}

Lexer.prototype.consume = function(state)
{
	if(this.lastIndex >= this.text.length)
		return false;
	for(token in state)
	{
		this.tokens[token].lastIndex = 0; //this.lastIndex;
		var result = this.tokens[token].exec(this.text.substring(this.lastIndex));
		if(result)
		{
			
			var ret = { tokenId: token, string: result[0], index: this.lastIndex + result.index, state: state[token] };
			this.lastIndex += result[0].length;
			return ret;
		}
	}

	return { tokenId: 'ERROR', index: ++this.lastIndex, state: 'ERROR', string: this.text.charAt(this.lastIndex - 1) };
	
}

Lexer.prototype.consumeAny = function()
{
	this.lastIndex++;
}


// BCRD Parser (Bastard Child of Recursive Descent)
function Parser(lexer, grammar, params)
{
	this.lexer = lexer;
	//this.grammar = grammar;
	this.grammar = grammar;
			
	if(params)
	{
		this.onEnterState = params.onEnterState || function(state, token) { };
		this.onDropState = params.onDropState || function(state, token) { };
		this.onLeaveState = params.onLeaveState || function(state, token) { };
		this.onParseError = params.onParseError || function(state, token) { };
	}
	else
	{
		this.onEnterState = function(state, token) {};
		this.onDropState = function(state, token) {};
		this.onLeaveState = function(state, token) {};
		this.onParseError = function(state, token) {};
	}

	this.state = null;

}

Parser.prototype.parse = function(text, start)
{
	//some browsers (IE8, ironically) have trouble with DOS-style CRLF
	//if(false && Syntaxed.NoCRLF)
	{
		text = text.replace(/\r\n/g, '\r');
	}
	//tab width is broken for white-space: pre in almost all browsers :-(
	
	
	this.lexer.init(text);
	if(start)
		this.lexer.lastIndex = start;
	this.states = ['INITIAL'];

	
	while(token = this.lexer.consume(this.grammar[this.states.peek()]))
	{
		if(token.state == 'ERROR')
		{
			//parse error
			this.onParseError(token.state, token);

		}
		else if(token.state == false)
		{
			var tokenId = token.tokenId;
			
			while(this.states.peek() != "INITIAL" && this.grammar[this.states.peek()][tokenId] == false)
			{				
				this.onLeaveState(this.states.pop(), token);
				token.string = "";
			}
		}
		else if(typeof(this.grammar[token.state]) == "undefined")
		{
			this.onDropState(token.state, token);
		}
		else
		{

			this.states.push(token.state);
			this.onEnterState(token.state, token);
		}
	}
}

function Syntaxed(textarea, language)
{
	var output = this.output = document.createElement("div");
	this.output.className = "syntaxed-output";

	var currentNode = this.currentNode = this.output;
	
	var textarea = this.textarea = textarea;

	var main = document.createElement("div");
	main.className = "syntaxed";
	
	var lineNums = this.lineNums = document.createElement("ol");
	main.appendChild(lineNums);

	this.timeout = null;

	this.templateSpan = document.createElement("span");
	


	var syntaxed = this;

	var lexer = this.lexer = new Lexer(language.tokens);
	var parser = this.parser = new Parser(this.lexer, language.grammar, {
		onEnterState: function(state, token)
		{
			//entered a new state; create a node for it an add to the current state's node
			var newNode = syntaxed.templateSpan.cloneNode(false) //document.createElement('span');
			newNode.className = state;
			newNode.index = token.index;
			newNode.appendChild(document.createTextNode(token.string));
			currentNode.appendChild(newNode);
			currentNode = newNode;
			if(language.afterStateEntered)
				language.afterStateEntered(state, token, currentNode, parser);
			//alert('Entering ' + state);
		},
		onLeaveState: function(state, token)
		{
			//left a state; add the token that caused it to the current state's node
			currentNode.appendChild(document.createTextNode(token.string));
			if(language.afterStateLeft)
			{
				currentNode = language.afterStateLeft(state, token, currentNode, parser);
			}
			else currentNode = currentNode.parentNode;

		},
		onDropState: function(state, token)
		{
			//"dropped" a state - this is a state that can't have child states, so just
			//create a node and add it, but retain the current state
			var newNode = syntaxed.templateSpan.cloneNode(false) //document.createElement('span');
			newNode.index = token.index;
			newNode.className = state;
			newNode.appendChild(document.createTextNode(token.string));
			currentNode.appendChild(newNode);
			if(language.afterStateDropped)
				language.afterStateDropped(state, token, currentNode, parser);
		},
		onParseError: function(state, token)
		{
			//parse error - we didn't get any of the possible expected tokens
			var newNode = syntaxed.templateSpan.cloneNode(false) //document.createElement('span');
			newNode.className = 'parse-error';
			newNode.appendChild(document.createTextNode(token.string));
			currentNode.appendChild(newNode);
			if(language.afterParseError)
				language.afterParseError(state, token, currentNode, parser)
		}

	});

	
	main.appendChild(output);

	if(textarea.nextSibling)
	{
		textarea.parentNode.insertBefore(main, textarea.nextSibling);
	}
	else
	{
		textarea.parentNode.appendChild(main);
	}

	main.appendChild(textarea.parentNode.removeChild(textarea));
	main.style.width = textarea.offsetWidth + 'px';
	main.style.height = textarea.offsetHeight + 'px';
	textarea.wrap = "off";

	textarea.onkeydown = function(e)
	{
		if(!e)
			e = window.event;



		// if the keypress is simple navigation we don't need to update
		// cursor keys, pgup, pgdown, home, end, ctrl, alt, capslock, shift, insert, function keys
		// maybe a O(n) string comparison of old == new would be better?  Probably slower.
		switch(e.keyCode)
		{
			case 16:	//shift
			case 17:	//ctrl
			case 18:	//alt
			case 19:	//pause/break
			case 20:	//caps lock
			case 27:	//esc
			case 33:	//pgup
			case 34:	//pgdown
			case 35:	//end
			case 36:	//home
			case 37:	//left
			case 38:	//up
			case 39:	//right
			case 40:	//down
			//case 45:	//ins (removed - shift-ins could be a paste)
				return;
		}




		var cursorPos = textarea.selectionStart;
		
		updateCursor();
		// if we do this stuff right away we'll be working on the old text
		// rather than the new text... so we want it to happen with the minimal
		// delay that will give us the new text.
		syntaxed.timeout = setTimeout(function() {
			var start = new Date().getTime();
			
			//optimization - we should get a HUGE speed boost by removing the output from
			//the display tree before operating on it, then re-inserting it after
			//Well, it's not so huge - about 17% increase in speed - but that is still significant.
			main.removeChild(output);
			
			
			//go backwards and find last initial state before current location
			if(cursorPos)
			{
				currentNode = output.childNodes[output.childNodes.length - 1];
				var lastIndex = 0;
				while(currentNode && currentNode.index >= cursorPos)
				{
					var prev = currentNode.previousSibling;
					lastIndex = currentNode.index;
					currentNode.parentNode.removeChild(currentNode);
					currentNode = prev;
				}

				if(currentNode)
				{
					var prev = currentNode.previousSibling;
					lastIndex = currentNode.index;
					currentNode.parentNode.removeChild(currentNode);
					currentNode = prev;
				}


				var pos = 0;
				if(currentNode)
					pos = lastIndex;

				currentNode = output;
				parser.parse(textarea.value + " ", pos);
			}
			else
			{
				// if cursor position is unavailable (*cough* IE) then we have to re-highlight everything
				// but hopefully someone editing code is smart enough not to use IE...
				// not that I can really fault IE for not including non-standard properties like selectionStart/End
				// but did they have to make it impossible to efficiently find selection offsets in a textarea?
				output.innerHTML = '';
				currentNode = output;
				parser.parse(textarea.value + " ");
			}
			
			//normalize line breaks (each browser version seems to freak out about a different style)
			//output.innerHTML = output.innerHTML.replace(/\r\n/g, '<br />');
			
			//gotta add back the output node (which was taken offline for editing)
			main.insertBefore(output, textarea);
			
			//sync textarea size to output size
			textarea.style.width = Math.max(output.offsetWidth, main.offsetWidth - lineNums.offsetWidth) + "px";
			textarea.style.height = (Math.max(output.offsetHeight, main.offsetHeight)) + "px";

			//sync line numbers
			while(lineNums.offsetHeight > textarea.offsetHeight)
				lineNums.removeChild(lineNums.childNodes[lineNums.childNodes.length - 1]);
			while(lineNums.offsetHeight < textarea.offsetHeight)
				lineNums.appendChild(document.createElement("li"));

			lineNums.style.paddingLeft = (2 + (lineNums.childNodes.length + "").length * 0.5) + "em";
			
			//move output & editor next to line numbers
			textarea.style.left = lineNums.offsetWidth + "px";
			output.style.left = lineNums.offsetWidth + "px";
			
			//make sure textarea is not scrolling independently of component
			textarea.scrollTop = 0;
			
			var end = new Date().getTime();
			window.status = (end - start);
			
		}, 0);
	};


	// initial parsing job
	parser.parse(textarea.value);
	textarea.style.width = Math.max(output.offsetWidth, main.offsetWidth - lineNums.offsetWidth) + "px";
	textarea.style.height = (Math.max(output.offsetHeight, main.offsetHeight)) + "px";
	while(lineNums.childNodes.length && lineNums.offsetHeight > output.offsetHeight)
		lineNums.removeChild(lineNums.childNodes[lineNums.childNodes.length - 1]);
	while(lineNums.offsetHeight < output.offsetHeight)
		lineNums.appendChild(document.createElement("li"));

	lineNums.style.paddingLeft = (2 + (lineNums.childNodes.length + "").length * 0.5) + "em";
	textarea.style.left = lineNums.offsetWidth + "px";
	output.style.left = lineNums.offsetWidth + "px";
				textarea.scrollTop = 0;
	
	
	var updateCursor = function ()
	{
		if((typeof textarea.selectionStart) !== 'number')
		{
			return;
		}
		
		if(textarea.selectionStart != textarea.selectionEnd)
		{
			cursor.style.display = 'none';
			return;
		}
		
		cursor.style.display = 'block';
		
		var str = textarea.value.replace(/\r\n/g, '\r').replace(/[^\s]/g, ' '); 
		str = str.substring(0, textarea.selectionStart) + String.fromCharCode(9615) + str.substring(textarea.selectionEnd + 1);
		cursor.innerHTML = str;
		console.log(textarea.selectionStart);
	};
	
	//with color:transparent on the textarea, we lose the cursor in all but IE.
	//so this makes a fake cursor.
	if((typeof textarea.selectionStart) === 'number')
	{
		var cursor = this.cursor = document.createElement('div');
		cursor.className = 'syntaxed-cursor';
		cursor.innerHTML = textarea.value.replace(/\r\n/g, '\r').replace(/[^\s]/g, ' '); 
		cursor.style.left = output.style.left;
		main.insertBefore(cursor, textarea);
		updateCursor();
		
		var scheduleUpdate = function () { setTimeout(updateCursor, 0); }
		
		textarea.addEventListener('mousedown', scheduleUpdate, false);
		textarea.addEventListener('keydown', scheduleUpdate, false);
	}
	

}


Syntaxed.Lang = {};

