/****************************************************************************************
	Helene 0.9
	Syntax Highlighting Textarea replacement with tab support and linenumbers
    Copyright (C) 2004 - 2005 Muze (http://www.muze.nl/)

    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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

You can contact Muze through email at info@muze.nl, for snailmail use

		Muze
		Piet Heinstraat 13
		7511JE Enschede
		the Netherlands

	---------------------------------------------------------------------------------
	TODO:
	- replace tabs with spaces in input textarea, since IE has a fixed width of 64px
	  for tabs, instead of 8*letterWidth. And if we're doing that anyway, make tab
	  size configurable.
	- fix grow / add shrink for long inputlines
	- selections
	- undo/redo
	- copy/paste multiple lines
	- highlighting
	- fix onfocus stuff for textarea
	- create an option to let tabs switch between textarea's/helene's, and/or provide
	  an alternative key (shift-tab? ctrl-tab?)
	- allways show the linenumbers
	- fix onchange stuff for reloading


	Short API for Helene:
	---------------------------------------------------------------------------------	
	TEXTAREA.helene						instance of the helene editor
		.source			reference to the original textarea
		.keyboard		contains all current keyboard bindings

		.copy()	
		.cut()	
		.doBackspace()
		.doDelete()
		.insertLinebreak()
		.insertTab()
		.paste()
		.redo()
		.select()
		.undo()

	TEXTAREA.helene.canvas				DIV: contains the editor widget
		.editor			reference to the helene editor
		.letterWidth	
		.borderWidth

	TEXTAREA.helene.canvas.content		OL: contains the editor content (lines)
		.editor 		reference to the helene editor
		.maxLineLength
		.length

		.clearContent()
		.getLetterWidth()
		.getLine()
		.init()
		.setContent()
		.setLine()

	TEXTAREA.helene.canvas.content.line LI: contains a single line of content
		.editor			reference to the helene editor
		.length
		.source
		
		.init()
		.setContent()

	TEXTAREA.helene.canvas.input		TEXTAREA: the yellow input line
		.editor 		reference to the helene editor
		.line			current line
		.column			real column position, only valid immediately after setPos or getPos
		.character		character position, idem

		.apply()
		.getPos()
		.hide()
		.init()
		.isDirty()
		.reset()
		.setPos()
		.show()

	TEXTAREA.helene.cursor				cursor object to keep track of real and virtual
										position of the cursor and selections
		.editor 		reference to the helene editor

		.getPos()
		.getRealPos()		returns a position object of the real cursor location
		.up()
		.down()
		.left()
		.right()
		.home()
		.end()
		.top()
		.bottom()
		.pageUp()
		.pageDown()
		.setPos()			moves the cursor to the virtual position, the real one may differ (tabs)
		.setRealPos()		moves the cursor, and changes the virtual position to the real one
		.wordRight()
		.wordLeft()

	TEXTAREA.helene.selection
		.editor 		reference to the helene editor
		.start			start position
		.end			end position

		.select(start, end)
		.moveStart(start)
		.moveEnd(end)
		.clear()

	helene.position
		.line
		.column
		.character

***************************************************************************************************/

var helene={

	options:{
		css:'./helene.css',
		tabSize:8,
		pageSize:20,
		autoIndent:true
	},

	config:{
		isMoz:(window.navigator.product=='Gecko'),
		undo:{
			CHANGE_TEXT:1,
			APPEND_LINE:2,
			DELETE_LINE:3
		}
	},

	// position is a utility object to return line, column and character position
	position:function(line, column, character) {
		this.line=line;
		this.column=column;
		this.character=character;
	},

	// util contains a few usefull methods to work with dom stuff, tabs, etc.
	util:{
		applyInherit:function(orig, interf) {
			for (method in interf) {
				orig[method] = interf[method];
			}
			return orig;
		},

		createElement:function (tagName) {
			switch(tagName) {
				case 'editorCanvas':
					return helene.util.applyInherit(document.createElement("DIV"), new helene.canvas());
				break;
				case 'editorInput':
					return helene.util.applyInherit(document.createElement("TEXTAREA"), new helene.input());
				break;
				case 'editorDocument':
					return helene.util.applyInherit(document.createElement("OL"), new helene.content());
				break;
				case 'editorLine':
					return helene.util.applyInherit(document.createElement("LI"), new helene.line());
				break;
				default:
					return document.createElement(tagName);
			}
		},

		createStyleSheet:function(href) {
			if(document.createStyleSheet) {
				document.createStyleSheet(href);
			} else {
				var newSS=document.createElement('link');
				newSS.rel='stylesheet';
				newSS.type='text/css';
				newSS.href=href;
				document.getElementsByTagName("head")[0].appendChild(newSS);
			}
		},

		tabs:{
			replace:function(line) {
				// replace tabs with correct number of nbsp's, FIXME: max tab size 8
				var tempTab="\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0";
				var tempLine=line;
				var tempIndex=-1;
				var counter=0;
				var spaceCount=0;
				var processedLine='';
				do {
					tempIndex=tempLine.indexOf("\t");
					if (tempIndex!=-1) {
						processedLine+=tempLine.substr(0, tempIndex);
						counter+=tempIndex;
						spaceCount=helene.options.tabSize-(counter%helene.options.tabSize);
						processedLine+=tempTab.substr(0, spaceCount);
						counter+=spaceCount;
						tempLine=tempLine.substr(tempIndex+1, tempLine.length);
					} else {
						processedLine+=tempLine;
					}
				} while (tempIndex!=-1);
				return processedLine;
			},
			columnToCharPos:function(column, line) {
				var colCounter=0;
				var charCounter=0;
				var tempLine=new String(line);
				var nextChar;
				var realTabSize;
				do {
					nextChar=tempLine.charAt(charCounter);
					// walk through the string a character at a time
					// FIXME: this can be sped up
					charCounter++;
					if (nextChar=='\t') {
						realTabSize=helene.options.tabSize-(colCounter%helene.options.tabSize);
						colCounter+=realTabSize;
					} else {
						colCounter++;
					}
				} while (nextChar && colCounter<column);
				return charCounter;
			},
			charPosToColumn:function(charPos, line) {
				var colCounter=1;
				var charCounter=0;
				var tempLine=line;
				var nextChar;
				var realTabSize;
				while (charCounter<(charPos-1)) {
					// walk through the string a character at a time
					// FIXME: this can be sped up
					nextChar=tempLine.charAt(charCounter);
					charCounter++;
					if (nextChar=='\t') {
						realTabSize=helene.options.tabSize-((colCounter-1)%helene.options.tabSize);
						colCounter+=realTabSize;
					} else {
						colCounter++;
					}
					if (!nextChar) {
						break;
					}
				}
				return colCounter;		
			},
			getRealColumn:function(column, line, skipTabs) {
				// returns the column the cursor should be put on
				// taking into account the cursor cannot be put in a tab
				// if skipTabs is true, the cursor will be put to the right
				// of the tab, if the cursor would otherwise be anywhere in
				// the tab. Otherwise, the cursor is put to the left.
				var colCounter=1;
				var charCounter=0;
				var tempLine=line;
				var nextChar;
				var realTabSize;
				while (colCounter<column) {
					// walk through the string a character at a time
					// FIXME: this can be sped up
					nextChar=tempLine.charAt(charCounter);
					charCounter++;
					if (nextChar=='\t') {
						realTabSize=helene.options.tabSize-((colCounter-1)%helene.options.tabSize);
						colCounter+=realTabSize;
					} else {
						colCounter++;
					}
					if (!nextChar) {
						break;
					}
				}
				if (!skipTabs && nextChar=='\t' && colCounter>column) {
					// if the last character was a tab, and the column is left of the colCounter column,
					// set the cursor to the left of the tab.
					colCounter-=realTabSize;
				}
				return colCounter;
			}
		}
	},

	// event contains all the code necessary to make IE and Mozilla play nice with events
	event:{
		cache:[],
		get:function(evt) {
			if (!evt) {
				evt=window.event;
			}
			if (!evt.target) {
				evt.target=evt.srcElement;
			}
			return evt;
		},
		cancel:function(evt) {
			if (evt.returnValue) {
				evt.returnValue=false;
			} 
			if (evt.preventDefault) {
				evt.preventDefault();
			}
			evt.cancelBubble=true;
			if (evt.stopPropagation) {
				evt.stopPropagation();
			}
			return false;
		},
		pass:function(evt) {
			return true;
		},
		attach:function(ob, event, fp, useCapture) {
			function createHandlerFunction(obj, fn){
				// FIXME: remember handler function somewhere so we can unattach it on unload
				// perhaps remember all events per object, so you can also specifically unattach
				// events for a specific object
				var o = new Object;
				o.myObj = obj;
				o.calledFunc = fn;
				o.myFunc = function(e){ 
					var e = helene.event.get(e);
					return o.calledFunc.call(o.myObj, e);
				}
				return o.myFunc;
			}
			var handler=createHandlerFunction(ob, fp);
			helene.event.cache[helene.event.cache.length]={ event:event, object:ob, handler:handler };
			if (ob.addEventListener){
				ob.addEventListener(event, handler, useCapture);
				return true;
			} else if (ob.attachEvent){
				return ob.attachEvent("on"+event, handler);
			} else {
				//FIXME: don't do alerts like this
				alert("Handler could not be attached");
			}
		},
		clean:function() {
			var item=null;
			for (var i=helene.event.cache.length-1; i>=0; i--) {
				item=helene.event.cache[i];
				item.object['on'+item.event]=null;
				if (item.object.removeEventListener) {
					item.object.removeEventListener(item.event, item.handler, item.useCapture);
				} else if (item.object.detachEvent) {
					item.object.detachEvent("on" + item.event, item.handler);
				}
				helene.event.cache[i]=null;
			}
			item=null;
		}
	},

	// keyboard contains all keyboard handling code, including key bindings for different editor behaviours
	keyboard:{
		behaviour:'default',
		binding:{
			'default':function(editor) {
				return {
					'default':	function() { editor.doCleanState(); return true; },
					'40':		function() { editor.cursor.down(); return false; },
					's40':		function() { editor.selection.down(); return false; },
					'38':		function() { editor.cursor.up(); return false; },
					's38':		function() { editor.selection.up(); return false; },
					'37':		function() { editor.cursor.left(); return false; },
					's37':		function() { editor.selection.left(); return false; },
					'39':		function() { editor.cursor.right(); return false; },
					's39':		function() { editor.selection.right(); return false; },
					'8':		function() { editor.doBackspace(); return false; },
					'46':		function() { editor.doDelete(); return false; },
					'13':		function() { editor.insertLinebreak(); return false; },
					'34':		function() { editor.cursor.pageDown(); return false; },
					's34':		function() { editor.selection.pageDown(); return false; },
					'33':		function() { editor.cursor.pageUp(); return false; },
					's33':		function() { editor.selection.pageUp(); return false; },
					'35':		function() { editor.cursor.end(); return false; },
					's35':		function() { editor.selection.end(); return false; },
					'c35':		function() { editor.cursor.bottom(); return false; },
					'cs35':		function() { editor.selection.bottom(); return false; },
					'36':		function() { editor.cursor.home(); return false; },
					's36':		function() { editor.selection.home(); return false; },
					'c36':		function() { editor.cursor.top(); return false; },
					'cs36':		function() { editor.selection.top(); return false; },
					'9':		function() { editor.insertTab(); return false; },
					'c86':		function() { editor.paste(); return false; },
					'c90':		function() { editor.undo(); return false; },
					'c89':		function() { editor.redo(); return false; },
					'cs90':		function() { editor.redo(); return false; },
					's16':		function() { return true; },
					'c17':		function() { return true; },
					'a18':		function() { return true; },
					'cs16':		function() { return true; },
					'cs17':		function() { return true; },
					'as16':		function() { return true; },
					'as18':		function() { return true; },
					'ac18':		function() { return true; },
					'ac17':		function() { return true; },
					'c67':		function() { return true; }
				}
			},
			'joe':function(editor) {
				return helene.keyboard.binding.merge(
					helene.keyboard.binding['default'](editor),
					{
						'c65':		function() { editor.cursor.home(); return false; },
						'c69':		function() { editor.cursor.end(); return false; },
						'c88':		function() { editor.cursor.wordRight(); return false; },
						'c90':		function() { editor.cursor.wordLeft(); return false; },
						'c75':		function() {
										editor.keyboard=helene.keyboard.binding['joeCtrlK'](editor);
										return false;
									},
						'cs189':	function() { editor.undo(); return false; },
						'cs109':	function() { editor.undo(); return false; },
						'cs54':		function() { editor.redo(); return false; }
					}
				);
			return false; },
			'joeCtrlK':function(editor) {
				return helene.keyboard.binding.merge(
					helene.keyboard.binding['default'](editor),
					{
						'85':		function() { editor.cursor.top(); editor.keyboard['default'](); return false; },
						'86':		function() { editor.cursor.bottom(); editor.keyboard['default'](); return false; },
						'default':	function() {
										editor.keyboard=helene.keyboard.binding['joe'](editor);
										return false;
									}
					}
				);
			}
		},
		getCharCode:function(evt) {
				return (evt.charCode ? evt.charCode : ((evt.keyCode) ? evt.keyCode : evt.which));
		},
		onKeyUp:function(evt) {
		},
		onKeyDown:function(evt) {
			var charCode;
			var charString = '';
			var keyResult;

			evt = helene.event.get(evt);
			if( evt ) {
				// Get the key pressed
				charCode = helene.keyboard.getCharCode(evt);

				// Create the encoded character string
				charString = charCode;

				if( evt.shiftKey ) {
					charString = 's' + charString;
				}
				if( evt.ctrlKey ) {
					charString = 'c' + charString;
				}
				if( evt.altKey ) {
					charString = 'a' + charString;
				}
				if( this.editor.keyboard[charString] ) {
					keyResult = this.editor.keyboard[charString](evt);
				} else {
					keyResult = this.editor.keyboard["default"](evt);
				}

				if( ! keyResult ) {
					helene.event.cancel(evt);
					this.editor.cancelKey = true; // Mozilla can only cancel it on onKeyPress, so remember to do that
				}
				return keyResult;
			}
		},
		onKeyPress:function(evt) {
			if (helene.config.isMoz && this.editor.cancelKey) {
				this.editor.cancelKey=false;
				return helene.event.cancel(evt);
			} else {
				return helene.event.pass(evt);
			}
		}
	},

	// mouse contains all the mouse handling code, including position calculation
	mouse:{
		getTarget:function(evt) {
			return (evt.target) ? evt.target : evt.srcElement;
		},
		getPos:function(evt) {
			var target=helene.mouse.getTarget(evt);
			// get click position
			if (evt.pageX) {
				var offsetX=evt.pageX - ((target.offsetLeft) ? target.offsetLeft : target.left);
				var offsetY=evt.pageY - ((target.offsetTop) ? target.offsetTop : target.top);
			} else if (evt.clientX || evt.clientY) {
				var offsetX = evt.clientX - ((target.offsetLeft) ? target.offsetLeft : 0);
				var offsetY = evt.clientY - ((target.offsetTop) ? target.offsetTop : 0);
			}
			// since we only have the page coordinates, calculate the offset of the editor
			// on the page
			var offsetParent=target.offsetParent;
			while (offsetParent) {
				offsetX-=offsetParent.offsetLeft;
				offsetY-=offsetParent.offsetTop;
				if (offsetParent.tagName=='BODY') {
					break;
				}
				offsetParent=offsetParent.offsetParent;
			}
			// add the offset from the canvas scrollbars (and an offset of half the letterwidth for easier mouse positioning)
			offsetX+=target.editor.canvas.scrollLeft + Math.floor(target.editor.canvas.letterWidth/2);
			offsetY+=target.editor.canvas.scrollTop;
			// calculate the lineNo and column
 			var x=1;
			var y=1;
			switch(target.tagName) {
				case 'TEXTAREA':
					y=target.line;
					x=Math.round(offsetX/target.editor.canvas.letterWidth);
				break;
				case 'DIV':
					target=target.content;
				// FALLTHROUGH					
				case 'OL':
					// clicked outside LI element, so first find the correct list item
					y=0;
					var temp=target.firstChild;
					while (temp && temp.offsetTop<offsetY) {
						temp=temp.nextSibling;
						y++;
					}
					x=Math.round(offsetX/target.editor.canvas.letterWidth);
				break;
				case 'LI':
					y=1;
					var temp=target;
					while (temp=temp.previousSibling) {
						y++;
					}
					x=Math.round(offsetX/target.editor.canvas.letterWidth);
				break;
			}
			return new helene.position(y, x, 0);
		},
		onMouseDown:function(evt) {
			evt=helene.event.get(evt);
			return helene.event.cancel(evt);
		},
		onMouseUp:function(evt) {
			evt=helene.event.get(evt);
			var pos=helene.mouse.getPos(evt);
			this.editor.cursor.setRealPos(pos.line, pos.column, pos.character);
			return helene.event.cancel(evt);
		}
	},

	// attach is the method that actually instantiates a new editor and attaches
	// it to the 'source' textarea
	attach:function(value) {
		value.helene=new helene.editor(value);
	},

	// editor is the core helene editor object
	editor:function(source) {
		var editor=this;
		this.source=source;
		this.canvas=helene.util.createElement('editorCanvas');
		// make sure the editor elements are in the document flow
		// before initializing them, so the browser will render them
		// and width/height of elements can be calculated
		this.source.parentNode.insertBefore(this.canvas, this.source);
		this.canvas.init(this);
		// load the content of the textarea
		this.canvas.content.setContent(this.source.value); 
		this.cursor=new helene.cursor(this);
		this.selection=new helene.selection(this);
		this.buffers={
			undo:new Array(),
			redo:new Array()
		}

		// editor methods
		this.save=function() {
			this.source.value=this.canvas.content.getContent();
		}
		this.doCleanState=function() {
			// do some cleaning up, clear up selections, etc.
			this.cursor.uptodate=false;
			return true;
		}
		this.insertTab=function() {
			var line = this.canvas.input.value;
			var pos = this.cursor.getRealPos();
			// FIXME: pos.character is offset by 1, is that correct?
			line = line.substr(0,pos.character-1) + "\t" + line.substr(pos.character-1);
			this.canvas.input.value = line;
			this.cursor.setRealPos(pos.line, pos.column+1, true);
			return true;
 		}
		this.insertLinebreak=function() {
			var pos=this.cursor.getRealPos();
			var head=this.canvas.input.value.substr(0, pos.character-1);
			var tail=this.canvas.input.value.substr(pos.character-1);
			var re = /([\t ]+)/;
			var match = re.exec(head);
			var newCursor = 1;
			if( helene.options.autoIndent && match ) {
				tail = match[1] + tail;
				newCursor = helene.util.tabs.charPosToColumn(match[1].length+1, tail);
			}
			this.canvas.input.value=head;
			this.canvas.input.apply();
			this.canvas.content.appendLine(pos.line, tail);
			this.cursor.setPos(pos.line+1, newCursor);
		}
		this.doBackspace=function() {
			var line = this.canvas.input.value;
			var pos = this.cursor.getRealPos();
			if (pos.character>1) {
				line = line.substr(0, pos.character-2) + line.substr(pos.character-1);
				// first set the cursor to the new location, so it skips over tabs
				this.cursor.setRealPos(pos.line, pos.column-1);
				// then remember the cursor position
				pos=this.cursor.getPos();
				this.canvas.input.value=line;
				// now set the cursor back
				this.cursor.setRealPos(pos.line, pos.column);
			} else if (pos.line>1) {
				var prevLine=this.canvas.content.getLine(pos.line-1);
				this.cursor.setPos(pos.line-1, prevLine.length+1);
				// remove line only after the input has moved, otherwise the input will apply changes to the wrong line
				this.canvas.content.removeLine(pos.line);
				this.canvas.input.value+=line;
				// now set the cursor again, so its moved back from the end of the input
				this.cursor.setPos(pos.line-1, prevLine.length+1);
			}			
		}
		this.doDelete=function() {
			var line = this.canvas.input.value;
			var pos = this.cursor.getRealPos();
			if (pos.character<=line.length) {
				// just remove a character
				line = line.substr(0, pos.character-1) + line.substr(pos.character);
				this.canvas.input.value=line;
				this.cursor.setPos(pos.line, pos.column);
			} else if (pos.line<this.canvas.content.length) {
				// delete removes linebreak
				var nextLine=this.canvas.content.getLine(pos.line+1);
				this.canvas.input.value+=nextLine.source;
				this.canvas.content.removeLine(pos.line+1);
				this.cursor.setPos(pos.line, pos.column);				
			}
		}
		this.undo=function() {
			if (this.canvas.input.isDirty()) {
				var redoElement={type:helene.config.undo.CHANGE_TEXT, text:this.canvas.content.getLine(this.cursor.line).source, newText:this.canvas.input.value, pos:this.cursor.getRealPos()}
				this.buffers.redo.push(redoElement); 
				this.canvas.input.value=this.canvas.content.getLine(this.cursor.line).source;
			} else if (this.buffers.undo.length) {
				var el=this.buffers.undo.pop();
				if (el) {
					switch(el.type) {
						case helene.config.undo.CHANGE_TEXT:
							this.canvas.input.value=el.text;
							this.cursor.setRealPos(el.pos.line, el.pos.column);
							this.buffers.redo.push({type:helene.config.undo.CHANGE_TEXT, text:el.newText, newText:el.text, pos:el.pos}); 
						break;
						case helene.config.undo.APPEND_LINE:
							this.canvas.content.removeLine(el.line);
							this.cursor.setRealPos(el.pos.line, el.pos.column);
							this.buffers.redo.push({type:helene.config.undo.DELETE_LINE, text:el.text, pos:el.pos});
						break;
						case helene.config.undo.DELETE_LINE:
							this.canvas.content.appendLine(el.pos.line-1, el.text);
							this.cursor.setRealPos(el.pos.line, el.pos.column);
							this.buffers.redo.push({type:helene.config.undo.APPEND_LINE, text:el.text, pos:el.pos});
						break;
						default:
						break;
					}
				}
			}
		}
		this.redo=function() {
			if (this.buffers.redo.length) {
				var el=this.buffers.redo.pop();
				if (el) {
					switch(el.type) {
						case helene.config.undo.CHANGE_TEXT:
							this.canvas.input.value=el.text;
							this.cursor.setRealPos(el.pos.line, el.pos.column);
							this.buffers.undo.push({type:helene.config.undo.CHANGE_TEXT, text:el.newText, newText:el.text, pos:el.pos}); 
						break;
						case helene.config.undo.APPEND_LINE:
							this.canvas.content.removeLine(el.line);
							this.cursor.setRealPos(el.pos.line, el.pos.column);
							this.buffers.undo.push({type:helene.config.undo.DELETE_LINE, text:el.text, pos:el.pos});
						break;
						case helene.config.undo.DELETE_LINE:
							this.canvas.content.appendLine(el.pos.line-1, el.text);
							this.cursor.setRealPos(el.pos.line, el.pos.column);
							this.buffers.undo.push({type:helene.config.undo.APPEND_LINE, text:el.text, pos:el.pos});
						break;
						default:
						break;
					}
				}
			}
		}
		this.keyboard=helene.keyboard.binding[helene.keyboard.behaviour](this);
		// now add onsubmit handler
		if (this.source.form) {
			helene.event.attach(this.source.form, 'submit', function() { editor.save(); });
		}
	},

	// canvas contains all visible elements of the editor
	canvas:function() {
		this.className='hlCanvas';
		this.init=function(editor) {
			this.editor=editor;

			// try to read font sizes and font style from the textarea
			if (this.editor.source.currentStyle) {
				var style=this.editor.source.currentStyle;
			} else {
				var style=document.defaultView.getComputedStyle(this.editor.source, "");
			}
			if (style) {
				if (this.style.setProperty) {
					for (var i=0; i<style.length; i++) {
						this.style.setProperty(style.item(i), style.getPropertyValue(style.item(i)), "");
					}
				} else {
					for (var i in style) {
						this.style[i]=style[i];
					}
				}
				this.style.fontFamily=style.fontFamily;
				this.style.fontSize=style.fontSize;
				this.style.color=style.color;

			}
			// position the canvas so it overlaps the textarea exactly
			this.canvasWidth=this.editor.source.offsetWidth;
			this.canvasHeight=this.editor.source.offsetHeight;
			this.style.width=this.canvasWidth+'px';
			this.style.height=this.canvasHeight+'px';

			// add the input line
			this.input=helene.util.createElement('editorInput'); //new helene.input(this);
			this.input.init(editor);
			this.appendChild(this.input);

			// add the content area
			this.content=helene.util.createElement('editorDocument'); //new helene.document(this);
			this.content.init(editor);
			this.appendChild(this.content);

			// calculate letterWidth
			this.letterWidth=this.content.getLetterWidth();

			// get border width to calculate inner width of canvas
			var temp=new String(this.style.borderLeftWidth);
			this.borderWidth=new Number(temp.replace(/px/,''));
			temp=new String(this.style.borderRightWidth);
			this.borderWidth=this.borderWidth+new Number(temp.replace(/px/,''));

			helene.event.attach(this, 'mousedown', helene.mouse.onMouseDown);
			helene.event.attach(this, 'mouseup', helene.mouse.onMouseUp);
		}
	},

	// input is the yellow input area, a textarea in disguise
	input:function() {
		this.className='hlInput';
		this.wrap='off';
		this.line=1;
		this.length=0;
		this.minWidth=0;
		this.grow=function(evt) {
			var cols=helene.util.tabs.charPosToColumn(this.value.length, this.value);
			var newwidth=(cols*this.editor.canvas.letterWidth)+1;
			if (newwidth>this.minWidth) {
				this.style.width=newwidth+'px';
				// FIXME: there should be another way to get the canvas to scroll to
				// the cursor position
				var pos=this.getPos();
				this.setPos(pos.line, pos.column, true);
			}
			evt=helene.event.get(evt);
			return helene.event.cancel(evt);			
		}
		this.init=function(editor) {
			this.editor=editor;
			this.style.fontFamily=this.editor.canvas.style.fontFamily;
			this.style.fontSize=this.editor.canvas.style.fontSize;
			this.style.color=this.editor.canvas.style.color;
			helene.event.attach(this, 'keydown', helene.keyboard.onKeyDown);
			helene.event.attach(this, 'keyup', helene.keyboard.onKeyUp);
			helene.event.attach(this, 'keypress', helene.keyboard.onKeyPress);
//			FIXME: growing shouldn't be done on scroll, but whenever the current
//			line grows larger than maxLineLength
//			also whenever the longest line shrinks, the reverse should be done
//			but never shrink the input line shorter than this.minWidth
			helene.event.attach(this, 'scroll', this.grow);
		}
		this.reset=function() {
			this.line=1;
			this.style.top='0px';
			this.value=this.editor.canvas.content.getLine(this.line).source;
			this.length=this.editor.canvas.content.getLine(this.line).length;
			this.style.height=this.editor.canvas.content.getLine(this.line).offsetHeight;
			// calculating offsetWidth of the content doesn't work, since IE hasn't rendered it yet
			// so use the letterWidth and the maxLength instead.
			this.minWidth=this.editor.source.offsetWidth-(this.editor.canvas.borderWidth*2)-56;
			var maxWidth=this.editor.canvas.content.maxLineLength*this.editor.canvas.letterWidth;
			if (this.minWidth && maxWidth) {
				if (this.minWidth>maxWidth) {
					maxWidth=this.minWidth;
				} else {
					maxWidth+=1; // make some room for the cursor in IE, there's a horizontal scrollbar anyway
				}
				this.style.width=maxWidth+'px';
			}
			this.show();
			this.focus();
		}
		this.hide=function() {
			this.style.display='none';
		}
		this.show=function() {
			this.style.display='block';
		}
		this.apply=function() {
			if (this.isDirty()) {
				this.editor.canvas.content.setLine(this.line, this.value);
			}
		}
		this.isDirty=function() {
			return (this.editor.canvas.content.getLine(this.line).source!=this.value);
		}
		this.getPos=function() {
			var input=this;
			function getCharacter() {
				// don't set the focus to the input line here, or the screen
				// will 'dance' around. Its not needed anyway.
				if (document.selection) {
					var cursor=document.selection.createRange();
					var fullRange=cursor.duplicate();
					fullRange.moveToElementText(input);
					cursor.setEndPoint('StartToStart', fullRange);
					var character=cursor.text.length;
				} else {
					var selection=window.getSelection();
					if (selection.anchorNode) {
						var character = input.selectionEnd;
					}
				}
				return character+1;
			}
			var character=getCharacter();
			var column=helene.util.tabs.charPosToColumn(character, this.value);
			return new helene.position(this.line, column, character);
		}
		this.setPos=function(line, column, skipTabs) {
			var editor=this.editor;
			var input=this;
			var result=true;
			function gotoCharacter(character) {
				if (document.selection) {
					var range=input.createTextRange();
					range.moveStart('character', character-1);
					range.collapse(true);
					range.select();
				} else {
					input.focus();
					input.setSelectionRange(character-1, character-1);
				}
			}
			function gotoLine(lineNo) {
				var result=true;
				input.apply();
				if (lineNo<1) {
					lineNo=1;
					result=false;
				} else if (lineNo>editor.canvas.content.length) {
					lineNo=editor.canvas.content.length;
					result=false;
				}
				var newPos=editor.canvas.content.getLine(lineNo).offsetTop;
				input.style.top=newPos+'px';
				input.value=editor.canvas.content.getLine(lineNo).source;
				if (newPos<editor.canvas.scrollTop) {
					editor.canvas.scrollTop=newPos;
				} else if (newPos>(editor.canvas.scrollTop+editor.canvas.offsetHeight-input.offsetHeight)) {
					editor.canvas.scrollTop=newPos-(editor.canvas.offsetHeight)+input.offsetHeight;
				}
				input.line=lineNo;
				input.length=editor.canvas.content.getLine(lineNo).length;
				return result;
			}
			gotoLine(line);
			if (column<1) {
				column=1;
				result=false;
			} else {
				var length=this.editor.canvas.content.getLine(this.line).length;
				if (column>length) {
					column=length+1;
				}
				result=false;
			}
			this.column=helene.util.tabs.getRealColumn(column, this.value, skipTabs);
			this.character=helene.util.tabs.columnToCharPos(this.column, this.value);
			gotoCharacter(this.character);
			window.status='row: '+this.line+'   col: '+this.column;
			return result;
		}
		this.getTokenLeft=function() {
			if (document.selection) {
				var range=document.selection.createRange();
				range.moveStart('character', -1);
				var token=range.text;
			} else if (this.selectionStart) {
				var token=this.value.substr(this.selectionStart-1, 1);
			}
			return token;
		}
		this.getTokenRight=function() {
			if (document.selection) {
				var range=document.selection.createRange();
				range.moveEnd('character', 1);
				var token=range.text;
			} else {
				var token=this.value.substr(this.selectionStart, 1);
			}
			return token;
		}
		this.moveLeft=function() {
			var token=this.getTokenLeft();
			if (token) {
				if (token=='\t') {
					this.setPos(this.line, this.column-1);
					var pos=this.getPos();
				} else {
					if (document.selection) {
						var cursor=document.selection.createRange();
						cursor.moveStart('character', -1);
						cursor.moveEnd('character', -1);
						cursor.select();
					} else {
						this.setSelectionRange(this.selectionStart-1, this.selectionStart-1);
					}
					this.column--;
					this.character--;
					var pos=new helene.position(this.line, this.column, this.character);
				}
				window.status='row: '+this.line+'   col: '+this.column;
			} else {
				var pos=false;
			}
			return pos;
		}
		this.moveRight=function() {
			var token=this.getTokenRight();
			if (token) {
				if (token=='\t') {
					this.setPos(this.line, this.column+1, true);
					var pos=this.getPos();
				} else {
					if (document.selection) {
						var cursor=document.selection.createRange();
						cursor.moveStart('character', 1);
						// cursor.moveEnd('character', 1);
						cursor.select();
					} else {
						this.setSelectionRange(this.selectionStart+1, this.selectionStart+1);
					}
					this.column++;
					this.character++;
					var pos=new helene.position(this.line, this.column, this.character);
				}
				window.status='row: '+this.line+'   col: '+this.column;
			} else {
				var pos=false;
			}
			return pos;
		}
	},

	// content is the numbered list that contains the html-ized content of the source textarea
	content:function() {
		this.className='hlContent';
		this.maxLineLength=0;
		this.length=0;
		this.init=function(editor) {
			// now read the content from the source, and insert it into
			// the document
			this.editor=editor;
			this.setContent('');
			helene.event.attach(this, 'mousedown', helene.mouse.onMouseDown);
			helene.event.attach(this, 'mouseup', helene.mouse.onMouseUp);
		}
		this.setContent=function(sourceText) {
			this.clearContent();
			this.editor.canvas.input.hide();
			sourceArray=sourceText.replace(/\r/, '').split('\n');
			var line=null;
			this.maxLineLength=0;;
			for (var i=0; i<sourceArray.length; i++) {
				line=helene.util.createElement('editorLine');
				line.init(this.editor);
				line.setContent(sourceArray[i]);
				this.appendChild(line);			
				if (this.maxLineLength<line.length) {
					this.maxLineLength=line.length;
				}
			}
			this.length=sourceArray.length;
			this.editor.canvas.input.reset();
		}
		this.clearContent=function() {
			for (var i=this.childNodes.length-1; i>=0; i--) {
				this.removeChild(this.childNodes[i]);
			}
			this.maxLineLength=0;
			this.length=0;
		}
		this.getContent=function() {
			this.editor.canvas.input.apply();
			var content='';
			if (this.length) {
				for (var i=1; i<this.length; i++) {
					content+=this.getLine(i)['source']+'\n';
				}
				content+=this.getLine(this.length)['source'];
			}
			return content;
		}
		this.getLine=function(number) {
			return this.childNodes[number-1];
		}
		this.setLine=function(number, value) {
			return this.childNodes[number-1].setContent(value);
		}
		this.appendLine=function(number, value) {
			var line=helene.util.createElement('editorLine');
			line.init(this.editor);
			line.setContent(value);
			if (number>=this.length) {
				this.appendChild(line);
			} else { 
				var nextLine=this.getLine(number+1);
				this.insertBefore(line, nextLine);
			}
			this.length++;
		}
		this.removeLine=function(number) {
			this.removeChild(this.childNodes[number-1]);
			this.length--;
		}
		this.getLetterWidth=function() {
			// calculate letter width 
			var textNode=document.createTextNode('M');
			var spanLocator=document.createElement('span');
			spanLocator.style.backgroundColor='red';	
			spanLocator.appendChild(textNode);
			var listItem=helene.util.createElement('editorLine');
			listItem.appendChild(spanLocator);
			this.appendChild(listItem);
			var letterWidth=spanLocator.offsetWidth;
			this.removeChild(listItem);
			return letterWidth;
		}
	},

	// line is a LI item, with additions, each of which contains one line of the source
	line:function() {
		this.className='hlLine';
		this.length=0;
		this.source=new String('');
		this.init=function(editor) {
			this.editor=editor;
			helene.event.attach(this, 'mousedown', helene.mouse.onMouseDown);
			helene.event.attach(this, 'mouseup', helene.mouse.onMouseUp);
		}
		this.setContent=function(sourceLine) {
			this.source=sourceLine.replace(new RegExp('\n','gim'), '').replace(new RegExp('\r','gim'), '');
			var processedLine=helene.util.tabs.replace(this.source);
			processedLine=processedLine.replace(/ /g, "\u00A0");
			this.length=processedLine.length;
			var text=document.createTextNode(processedLine);
			if (this.firstChild) {
				this.replaceChild(text, this.firstChild);
			} else {
				this.appendChild(text);
			}
		}
	},

	// cursor is the virtual cursor which keeps track of where the cursor should be
	// it also has all the cursor handling methods (left, right, up, down, etc)
	cursor:function(editor) {
		this.editor=editor;
		this.column=1;
		this.line=1;
		this.uptodate=false;

		this.getRealPos=function() {
			return this.editor.canvas.input.getPos();
		}

		this.getPos=function() {
			if (!this.uptodate) {
				return this.editor.canvas.input.getPos();
			} else {
				return new helene.position(this.line, this.column, 0);
			}
		}

		this.setPos=function(line, column, skipTabs) {
			var result=this.editor.canvas.input.setPos(line, column, skipTabs);
			this.line=this.editor.canvas.input.line;
			this.column=column;
			this.uptodate=true;
			return result;
		}

		this.setRealPos=function(line, column, skipTabs) {
			var result=this.setPos(line, column, skipTabs);
			this.column=this.editor.canvas.input.column;
			return result;
		}

		this.down=function() {
			var pos=this.getPos();
			return this.setPos(pos.line+1, pos.column);
		}

		this.up=function() {
			var pos=this.getPos();
			return this.setPos(pos.line-1, pos.column);
		}

		this.left=function() {
			var pos=this.editor.canvas.input.moveLeft();
			if (pos) {
				this.column=pos.column;
				if (this.column<10) {
					this.editor.canvas.scrollLeft=0;
				} else if (helene.config.isMoz) {
					var xPos=this.column*this.editor.canvas.letterWidth+44-this.editor.canvas.letterWidth;
					var currX=this.editor.canvas.scrollLeft;
					if (currX>xPos) {
						this.editor.canvas.scrollLeft=xPos;
					}
				}
			} else {
				var line=this.editor.canvas.content.getLine(this.line-1);
				if (line) {
					this.setRealPos(this.line-1, line.length+1);
					if (helene.config.isMoz) {
						var xPos=this.column*this.editor.canvas.letterWidth;
						var currX=this.editor.canvas.scrollLeft+this.editor.canvas.canvasWidth-44;
						if (currX<xPos) {
							this.editor.canvas.scrollLeft=(xPos-(this.editor.canvas.canvasWidth-44));
						}
					}
				}			
			}
		}

		this.right=function() {
			var pos=this.editor.canvas.input.moveRight();
			if (pos) {
				this.column=pos.column;
				if (helene.config.isMoz) {
					var xPos=this.column*this.editor.canvas.letterWidth;
					var currX=this.editor.canvas.scrollLeft+this.editor.canvas.canvasWidth-44;
					if (currX<xPos) {
						this.editor.canvas.scrollLeft=(xPos-(this.editor.canvas.canvasWidth-44));
					}
				}
			} else {
				var line=this.editor.canvas.content.getLine(this.line+1);
				if (line) {
					this.setRealPos(this.line+1, 1);
					this.editor.canvas.scrollLeft=0;
				}			
			}
		}

		this.home=function() {
			this.editor.canvas.scrollLeft=0;
			return this.setRealPos(this.line, 1);
		}

		this.end=function() {
			var result=this.setRealPos(this.line, this.editor.canvas.content.getLine(this.line).length+1);
			if (helene.config.isMoz) {
				var xPos=this.column*this.editor.canvas.letterWidth;
				var currX=this.editor.canvas.scrollLeft+this.editor.canvas.canvasWidth-44;
				if (currX<xPos) {
					this.editor.canvas.scrollLeft=(xPos-(this.editor.canvas.canvasWidth-44));
				}
			}
			return result;
		}

		this.top=function() {
			return this.setRealPos(1,1);
		}

		this.bottom=function() {
			var line=this.editor.canvas.content.getLine(this.editor.canvas.content.length);
			return this.setRealPos(this.editor.canvas.content.length, line.length+1);
		}

		this.pageUp=function() {
			var pos=this.getPos();
			return this.setPos(pos.line-helene.options.pageSize, pos.column);
		}

		this.pageDown=function() {
			var pos=this.getPos();
			return this.setPos(pos.line+helene.options.pageSize, pos.column);
		}

		this.wordRight=function() {
		}

		this.wordLeft=function() {
		}

	},

	// selection keeps track of a selection if it is available, and has all the cursor
	// methods for cursor movement which changes the position/size of the selection
	selection:function(editor) {
		this.editor=editor;
		this.column=0;
		this.line=0;

		this.getRealPos=function() {
			return this.editor.canvas.input.getPos();
		}

		this.getPos=function() {
			if (!this.uptodate) {
				return this.editor.canvas.input.getPos();
			} else {
				return new helene.position(this.line, this.column, 0);
			}
		}

		this.setPos=function(line, column) {
			var result=this.editor.canvas.input.setPos(line, column);
			this.line=this.editor.canvas.input.line;
			this.column=column;
			this.uptodate=true;
			return result;
		}

		this.setRealPos=function(line, column) {
			var result=this.setPos(line, column);
			this.column=this.editor.canvas.input.column;
			return result;
		}

		this.down=function() {
			var pos=this.getPos();
			return this.setPos(pos.line+1, pos.column);
		}

		this.up=function() {
			var pos=this.getPos();
			return this.setPos(pos.line-1, pos.column);
		}

		this.left=function() {
			var pos=this.getRealPos();
			var line=this.editor.canvas.content.getLine(pos.line-1);
			if (line && pos.character==1) {
				// left moves to the end of the previous line
				return this.setRealPos(pos.line-1, line.length+1);
			} else {
				return this.setRealPos(pos.line, pos.column-1);
			}
		}

		this.right=function() {
			var pos=this.getRealPos();
			var line=this.editor.canvas.content.getLine(pos.line+1);
			if (line && pos.character==line.length) {
				// right moves to the start of the next line
				return this.setRealPos(pos.line+1, 1); 
			} else {
				return this.setRealPos(pos.line, pos.column+1);
			}
		}

		this.home=function() {
			return this.setRealPos(this.line, 1);
		}

		this.end=function() {
			return this.setRealPos(this.line, this.editor.canvas.content.getLine(this.line).length+1);
		}

		this.top=function() {
			return this.setRealPos(1,1);
		}

		this.bottom=function() {
			var line=this.editor.canvas.content.getLine(this.editor.canvas.content.length);
			return this.setRealPos(this.editor.canvas.content.length, line.length+1);
		}

		this.pageUp=function() {
			var pos=this.getPos();
			return this.setPos(pos.line-helene.options.pageSize, pos.column);
		}

		this.pageDown=function() {
			var pos=this.getPos();
			return this.setPos(pos.line+helene.options.pageSize, pos.column);
		}

		this.wordRight=function() {
		}

		this.wordLeft=function() {
		}

	},

	// this is the default helene initialization method, which
	// inserts the helene stylesheet and attaches helene editors
	// on each textarea with class 'helene' set.
	init:function() {
		helene.util.createStyleSheet(helene.options.css);

		// attach helene to any textarea with class 'helene'
		var textareas=document.getElementsByTagName('TEXTAREA');
		var length=textareas.length;

		// this array is needed to prevent mozilla from silently 
		// changing the array while you add textareas to the 
		// document.
		var mozillasucks=new Array();
		for (var i=0; i<length; i++) {
			mozillasucks[i]=textareas[i];
		}
		for (var i=0; i<length; i++) {
			if (mozillasucks[i].className.match(/(.*helene.*)/)) {
				helene.attach(mozillasucks[i]);
			}
		}		
	}
}

helene.event.attach(window, 'load', helene.init);
helene.event.attach(window, 'unload', helene.event.clean);
