/**
 * Extended Textarea
 *
 * Copyright 2008 Alan Kang
 *  - mailto:jania902@gmail.com
 *  - http://jania.pe.kr
 *
 * http://jania.pe.kr/aw/moin.cgi/ExtendedTextarea
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */
var xtx = {};



xtx.Editor = function(textareaId) {
	this._textarea = $(textareaId);
	this._tm = new xtx.TextManipulator();
	this._caret = new xtx.Caret(this._textarea);
	
	this.getTextarea = function() {
		return this._textarea;
	}
	this.setText = function(text) {
		this._textarea.value = text;
	}
	this.getText = function() {
		return this._textarea.value.replace(/\r\n/img, "\n");
	}
	this.indent = function() {
		if(this._caret.hasSelection()) {
			this._modifyLines(this._tm.indentText);
		} else {
			this._caret.insert("   ");
		}
	}
	this.outdent = function() {
		this._modifyLines(this._tm.outdentText);
	}
	this.selectLine = function() {
		var result = this._tm.selectLine(this._caret.get(), this.getText());
		this._caret.set(result.pos);
		return result.pos;
	}
	this.moveUp = function() {
		this._performLineOperationAndSetCaret(this._tm.moveUp.bind(this._tm));
	}
	this.moveDown = function() {
		this._performLineOperationAndSetCaret(this._tm.moveDown.bind(this._tm));
	}
	this.copyUp = function() {
		this._performLineOperationAndSetCaret(this._tm.copyUp.bind(this._tm));
	}
	this.copyDown = function() {
		this._performLineOperationAndSetCaret(this._tm.copyDown.bind(this._tm));
	}
	this.deleteLine = function() {
		this._performLineOperationAndSetCaret(this._tm.deleteLine.bind(this._tm));
	}
	
	this._performLineOperationAndSetCaret = function(operation) {
		this.selectLine();
		var result = operation(this._caret.get(), this.getText());
		this.setText(result.text);
		this._caret.set(result.pos);
		
		return result.pos;
	}
	
	this._modifyLines = function(modifierFunction) {
		if(this._caret.hasSelection()) {
			var pos = this.selectLine();
			var selected = this._caret.getText();
			
			// Trident needs extra \n.
			var modified = modifierFunction(selected);
			this._caret.insert(modified);
			this._caret.set([pos[0], pos[0] + modified.length]);
		} else {
			var pos = this._caret.get();
			
			this.selectLine();
			var selected = this._caret.getText();
			
			var modified = modifierFunction(selected);
			this._caret.insert(modified);
			this._caret.set(pos[0] - (selected.length - modified.length));
		}
	}
}



xtx.Caret = function(textarea) {
	this._textarea = textarea;
	this._doc = this._textarea.ownerDocument;
	
	this.moveStart = function(delta) {
		if(xtx.isTrident) {
			var r = this._rng();
			r.moveStart("character", delta);
			r.select();
		} else {
			this._textarea.selectionStart += delta;
		}
	}
	this.moveEnd = function(delta) {
		if(xtx.isTrident) {
			var r = this._rng();
			r.moveEnd("character", delta);
			r.select();
		} else {
			this._textarea.selectionEnd += delta;
		}
	}
	this.insert = function(text) {
		if(xtx.isTrident) {
			var r = this._rng();
			var expectedDelta = r.text.replace(/\r\n/img, "\n").length - text.length;
			var lenBeforeInsert = this._textarea.value.length;
			r.text = text;
			var actualDelta = lenBeforeInsert - this._textarea.value.length;
			
			// Trident deletes "\r\n" sometimes.
			if(expectedDelta !== actualDelta) r.text = "\n";
		} else {
			var value = this._textarea.value;
			var start = this._textarea.selectionStart;
			var end = this._textarea.selectionEnd;
			this._textarea.value = value.substring(0, start) + text + value.substring(end);
			this._textarea.selectionStart = this._textarea.selectionEnd = start + text.length;
		}
	}
	this.collapse = function(toStart) {
		if(xtx.isTrident) {
			var r = this._rng();
			r.collapse(toStart);
			r.select();
		} else {
			if(toStart) {
				this._textarea.selectionEnd = this._textarea.selectionStart;
			} else {
				this._textarea.selectionStart = this._textarea.selectionEnd;
			}
		}
	}
	this.move = function(delta) {
		this.moveStart(delta);
		this.collapse(true);
	}
	this.setStart = function(index) {
		this.moveStart(index - this.get()[0]);
	}
	this.setEnd = function(index) {
		this.moveEnd(index - this.get()[1]);
	}
	this.get = function() {
		if(xtx.isTrident) {
			var marker = "\x01";
			var r = this._rng();
			var bm = r.getBookmark();
			var len = this.getText().length;
			r.collapse(true);
			r.text = marker;
			
			var text = this._textarea.value.replace(/\r\n/img, "\n");
			var start = text.indexOf(marker);
			var end = start + len;
			this._textarea.value = text.substring(0, start) + text.substring(start + marker.length);
			
			if(start === end && start === text.length - marker.length) {
				// this fixes IE bug
				r.collapse(false);
			} else {
				r.moveToBookmark(bm);
			}
			r.select();
			return [start, end];
		} else {
			return [this._textarea.selectionStart, this._textarea.selectionEnd];
		}
	}
	this.set = function(indiceOrIndex) {
		if(typeof indiceOrIndex === "number") {
			this.setStart(indiceOrIndex);
			this.collapse(true);
		} else {
			this.setEnd(indiceOrIndex[1]);
			this.setStart(indiceOrIndex[0]);
		}
	}
	this.getText = function() {
		if(xtx.isTrident) {
			return this._rng().text.replace(/\r\n/img, "\n");
		} else {
			var r = this.get();
			return this._textarea.value.substring(r[0], r[1]);
		}
	}
	this.hasSelection = function() {
		return this.getText().length > 0;
	}

	this._rng = function() {
		return this._doc.selection.createRange();
	}
}



xtx.TextManipulator = function() {
	this.selectLine = function(caretPos, text) {
		if(caretPos[0] !== caretPos[1] && caretPos[1] > 0 && text.charAt(caretPos[1] - 1) === "\n") caretPos[1] -= 1;
			
		var head = text.substring(0, caretPos[0]);
		var tail = text.substring(caretPos[1]);
		
		var headStart = head.lastIndexOf("\n") + 1;
		var tailEnds = tail.indexOf("\n");
		if(tailEnds === -1) tailEnds = tail.length;
		tailEnds += caretPos[1];
		
		return {text: text, pos:[headStart, tailEnds]};
	}
	this.indentText = function(text) {
		return text.replace(/(^|\n)/g, "$1   ");
	}
	this.outdentText = function(text) {
		return text.replace(/(^|\n)   /g, "$1");
	}
	this.moveUp = function(belowPos, text) {
		if(belowPos[0] === 0) return {text:text, pos:belowPos};
		
		var abovePos = this.selectLine([belowPos[0] - 1, belowPos[0] - 1], text).pos;
		
		var head = text.substring(0, abovePos[0]);
		var below = text.substring(belowPos[0], belowPos[1]);
		var above = text.substring(abovePos[0], abovePos[1]);
		var tail = text.substring(belowPos[1]);
		
		var modifiedText = head + below + "\n" + above + tail;
		
		return {text: modifiedText, pos: [abovePos[0], abovePos[0] + below.length]};
	}
	this.moveDown = function(abovePos, text) {
		if(abovePos[1] === text.length) return {text:text, pos:abovePos};
		
		var belowPos = this.selectLine([abovePos[1] + 1, abovePos[1] + 1], text).pos;
		
		var head = text.substring(0, abovePos[0]);
		var below = text.substring(belowPos[0], belowPos[1]);
		var above = text.substring(abovePos[0], abovePos[1]);
		var tail = text.substring(belowPos[1]);
		
		var modifiedText = head + below + "\n" + above + tail;
		
		return {text: modifiedText, pos: [abovePos[0] + below.length + 1, abovePos[0] + below.length + 1 + above.length]};
	}
	this.copyUp = function(caretPos, text) {
		var head = text.substring(0, caretPos[1] + 1); // +1 for \n
		var cloned = text.substring(caretPos[0], caretPos[1]) + "\n";
		var tail = text.substring(caretPos[1] + 1);
		
		var modifiedText = head + cloned + tail;
		return {text: modifiedText, pos: caretPos};
	}
	this.copyDown = function(caretPos, text) {
		var head = text.substring(0, caretPos[1] + 1); // +1 for \n
		var cloned = text.substring(caretPos[0], caretPos[1]) + "\n";
		var tail = text.substring(caretPos[1] + 1);
		
		var modifiedText = head + cloned + tail;
		return {text: modifiedText, pos: [caretPos[1] + 1, caretPos[1] + 1 + cloned.length - 1]}; // -1 for \n
	}
	this.deleteLine = function(caretPos, text) {
		var head = text.substring(0, caretPos[0]);
		var tail = text.substring(caretPos[1] + 1); // +1 for \n
		return {text: head + tail, pos: [caretPos[0], caretPos[0]]};
	}
	
	// for debugging
	this._debugModifier = function(decoratedText, modifierFunction) {
		var start = decoratedText.indexOf('[');
		var end = decoratedText.indexOf(']') - 1;
		var text = decoratedText.replace(/(\[|\])/img, "");
		
		var result = modifierFunction([start, end], text);
		var newPos = result.pos;
		
		return result.text.substring(0, newPos[0]) + "[" + result.text.substring(newPos[0], newPos[1]) + "]" + result.text.substring(newPos[1]);
	}
	this._selectLine = function(decoratedText) {
		return this._debugModifier(decoratedText, this.selectLine.bind(this));
	}
	this._moveUp = function(decoratedText) {
		return this._debugModifier(decoratedText, this.moveUp.bind(this));
	}
	this._moveDown = function(decoratedText) {
		return this._debugModifier(decoratedText, this.moveDown.bind(this));
	}
	this._copyUp = function(decoratedText) {
		return this._debugModifier(decoratedText, this.copyUp.bind(this));
	}
	this._copyDown = function(decoratedText) {
		return this._debugModifier(decoratedText, this.copyDown.bind(this));
	}
	this._deleteLine = function(decoratedText) {
		return this._debugModifier(decoratedText, this.deleteLine.bind(this));
	}
}



xtx.ShortcutInterpreter = function(editor) {
	this._editor = editor;
	
	this.bind = function() {
		this._editor.getTextarea().onkeydown = this.onKeydown.bindAsEventListener(this);
	}
	
	this.onKeydown = function(e) {
		var command = this.interprete(e);
		
		if(command) {
			this.execute(command)
			stopEvent(e);
		}
	}
	
	this.interprete = function(e) {
		if(e.keyCode === 13 && e.ctrlKey) {
			return "RunAsCode";
		} else if(e.keyCode === 9 && !e.shiftKey) {
			return "Indent";
		} else if(e.keyCode === 9 && e.shiftKey) {
			return "Outdent";
		} else if(e.keyCode === 38 && e.altKey && !e.ctrlKey) {
			return "MoveUp";
		} else if(e.keyCode === 40 && e.altKey && !e.ctrlKey) {
			return "MoveDown";
		} else if(e.keyCode === 38 && e.altKey && e.ctrlKey) {
			return "CopyUp";
		} else if(e.keyCode === 40 && e.altKey && e.ctrlKey) {
			return "CopyDown";
		} else if(e.keyCode === 68 && e.ctrlKey) {
			return "DeleteLine";
		} else {
			return null;
		}
	}

	this.execute = function(command) {
		this["execute" + command]();
	}

	this.executeRunAsCode = function() {eval(this._editor.getText());}
	this.executeIndent = function() {this._editor.indent();}
	this.executeOutdent = function() {this._editor.outdent();}
	this.executeMoveUp = function() {this._editor.moveUp();}
	this.executeMoveDown = function() {this._editor.moveDown();}
	this.executeCopyUp = function() {this._editor.copyUp();}
	this.executeCopyDown = function() {this._editor.copyDown();}
	this.executeDeleteLine = function() {this._editor.deleteLine();}
}



/* ------------------------------------------------------------------
 * utility functions
 * ------------------------------------------------------------------ */

xtx.isTrident = navigator.appName === "Microsoft Internet Explorer";
xtx.isWebkit = navigator.userAgent.indexOf('AppleWebKit/') > -1;
xtx.isGecko = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1;
xtx.isKHTML = navigator.userAgent.indexOf('KHTML') !== -1;
xtx.isPresto = navigator.appName === "Opera";

function $(id) {
	return document.getElementById(id);
}

$A = function(arraylike) {
	var len = arraylike.length, a = [];
	while (len--) {
		a[len] = arraylike[len];
	}
	return a;
}

function stopEvent(e) {
	if(e.preventDefault) {
		e.preventDefault();
		e.stopPropagation();
	} else {
		e.returnValue = false;
		e.cancelBubble = true;
	}
}

Function.prototype.bind = function() {
	var m = this, arg = $A(arguments), o = arg.shift();
	return function() {
		return m.apply(o, arg.concat($A(arguments)));
	};
}

Function.prototype.bindAsEventListener = function() {
	var m = this, arg = $A(arguments), o = arg.shift();
	return function(event) {
		return m.apply(o, [event || window.event].concat(arg));
	};
}