// Why on earth did you re-implement the DOM? What's wrong it it?
// The DOM isn't JUST TOO SLOW. For years web developers have worked around this by writing large, multi-element HTML strings (generated by hand or from templates).
// LightningDOM gives you the speed of large HTML strings without the headaches of parsing, searching, regexing, etc.

// LightningDOM uses an ultra-fast flyweight object model; objects storing little of their own data are created on-the-fly as you need them.
// These objects are a convenience API on top of an array of HTML strings. This was written initially for IE, so it is optimized for IE
// The API is a simplified version of the core XML/HTML DOM. It doesn't include separate CommentNodes or TextNodes -- to get to text content, use elem.innerHTML or elem.tail
var LightningDOMNode = Class.create({
	// important internal array indexes:
	_START_BRACKET: 0,
	_START_SLASH: 1,
	_TAG_NAME: 2,
	_ATTRIBUTES: 3,
	_END_SLASH: 4,
	_END_BRACKET_AND_TAIL: 5,
	initialize: function(dom, nStartTagIndex) {
		this._dom = dom;
		this._nStartTagIndex = nStartTagIndex;
		this._aTag = this._dom._aMarkup[this._nStartTagIndex];
	},
	// we're into generating new objects, rather than using a factory & returning cached objects
	// b/c we want to avoid circular references, get things garbage-collected and keep things lean
	// these are super-lightweight & fast to create, all the data is stored in ._dom's array anyway...
	firstChild: function() {
		// if this is self-closing or if the next element is it's closing tag, it has no firstChild
		return (this._aTag[this._END_SLASH] || this._dom._aMarkup[this._nStartTagIndex + 1][this._START_SLASH]) ? null :
			new LightningDOMNode(this._dom, this._nStartTagIndex + 1);
	},
	nextSibling: function() {
		var nSiblingIndex = this._endTagIndex() + 1;
		// there are no more siblings if it's out of range or it's an end-tag (of the parent)
		if (nSiblingIndex >= this._dom._aMarkup.length || this._dom._aMarkup[nSiblingIndex][this._START_SLASH])
			return null;
		else
			return new LightningDOMNode(this._dom, nSiblingIndex);
	},
	tagName: function() {
		return this._aTag[this._TAG_NAME];
	},
	type: function(strType) {
		if (strType)
			this.setAttribute("type", strType);
		else
			strType = this.getAttribute("type");
		return strType;
	},
	id: function(strID) {
		if (strID)
			this.setAttribute("id", strID);
		else
			strID = this.getAttribute("id");
		return strID;
	},
	className: function(strClass) {
		if (strClass)
			this.setAttribute("class", strClass);
		else
			strClass = this.getAttribute("class");
		return strClass;
	},
	// TODO: Only apply the value() function to form elements???
	value: function(strValue) {
		if (strValue)
			this.setAttribute("value", strValue);
		else
			strValue = this.getAttribute("value");
		return strValue;
	},
	// getter/setter b/c it's too much work for client-code
	// to get and set the whole collection just to modify a single property
	getAttribute: function(strAttribute) {
		return this.attributes()[strAttribute];
	},
	setAttribute: function(strAttribute, strValue) {
		var objAttributes = this.attributes();
		objAttributes[strAttribute] = strValue;
		this.attributes(objAttributes);
	},
	// getter/setter for the whole collection
	attributes: function(objAttributes) {
		// set them
		if (objAttributes) {			
			var astrResult = [];
			for (var strAttrib in objAttributes)
				astrResult.push(" ", strAttrib, '="', objAttributes[strAttrib], '"');
			//Utils.startTime("(" + (astrResult.length / 5) + " attributes)");
			//Utils.stopTime("(" + (astrResult.length / 5) + " attributes)");
			this._aTag[this._ATTRIBUTES] = astrResult.join('');
			this._objAttributes = objAttributes;
			return objAttributes;
		}
		
		if (this._objAttributes) return this._objAttributes;

		// get them
		objAttributes = {};
		var strAttributes = this._aTag[this._ATTRIBUTES];
		
		// OPTIMIZATION for 5 or fewer attributes
		var re = /^(?:\s*([a-z]+)=(?:(?:"([^"]*)")|(?:'([^']*)')|([a-z0-9\._:-]*))(?:\s*([a-z]+)=(?:(?:"([^"]*)")|(?:'([^']*)')|([a-z0-9\._:-]*))(?:\s*([a-z]+)=(?:(?:"([^"]*)")|(?:'([^']*)')|([a-z0-9\._:-]*))(?:\s*([a-z]+)=(?:(?:"([^"]*)")|(?:'([^']*)')|([a-z0-9\._:-]*))(?:\s*([a-z]+)=(?:(?:"([^"]*)")|(?:'([^']*)')|([a-z0-9\._:-]*)))?)?)?)?)?$/i;
		var match = re.exec(strAttributes);
		if (match) {
			for (var nMatch = 1; match[nMatch]; nMatch += 4)
				objAttributes[match[nMatch]] = match[nMatch + 1] ? match[nMatch + 1] : match[nMatch + 2] ? match[nMatch + 2] : match[nMatch + 3];
		}
		// fallback
		else {
			var reAttrib = new RegExp().compile("([a-z]+)=(?:" + /* double-quoted values */ '(?:"([^"]*)")|' + /* single-quoted values */ "(?:'([^']*)')|" + /* non-quoted values */ "([a-z0-9\\._:-]*))", "gi")
			while (match = reAttrib.exec(strAttributes)) {
				objAttributes[match[1]] = match[2] ? match[2] : match[3] ? match[3] : match[4];
			}
		}
		return this._objAttributes = objAttributes;
	},
	cloneNode: function() {
		// make a copy of all the markup
		// & make each sub-array a clone (not a reference to the this._dom ones)
		var arr = []
		for(var nArr = this._nStartTagIndex, nEndIndex = this._endTagIndex(), aMarkup = this._dom._aMarkup; nArr <= nEndIndex; ++nArr)
			arr.push(aMarkup[nArr].slice(0));
		return new LightningDOMNode({_aMarkup: arr}, 0);
	},
	innerHTML: function(strHTML) {
		// getter
		if (!strHTML) {
			var aMarkup = this._dom._aMarkup, nEndTagIndex = this._endTagIndex();
			var aResult = [aMarkup[this._nStartTagIndex][this._END_BRACKET_AND_TAIL].substr(1)];
			aMarkup = aMarkup.slice(this._nStartTagIndex + 1, nEndTagIndex);
			strHTML = aResult.concat.apply(aResult, aMarkup).join("");
		}
		// setter
		else {
			// parseMarkup is for OUTERhtml -- it doesn't support leading text (the tail of your start tag)
			//var aMarkup = this._dom.parseMarkup(strHTML);
			// REMOVED CHECK: 8/26/2008 PAR - I needed to support little markup snippets -- <b>, <i> and the like. Commented this out & everything works fine.
			//if (strHTML.indexOf("<") != -1) throw new Error("not yet implemented: setting tags into .innerHTML");
			
			// create end-tag if it was self-closing
			if (this._aTag[this._END_SLASH]) this._addEndTag();
			
			// remove any child elements you may have & reset your nEndTagIndex cache appropriately
			var nChildElements = this._endTagIndex() - this._nStartTagIndex - 1;
			if (nChildElements > 0) {
				this._dom._aMarkup.splice(this._nStartTagIndex + 1, nChildElements);
				if (this._nEndTagIndex) this._nEndTagIndex -= nChildElements;
			}
			
			// stuff the innerHTML into the tail of your start tag
			this._aTag[this._END_BRACKET_AND_TAIL] = ">" + strHTML;
		}
		return strHTML;
	},
	innerText: function(strHTML) {
		var aMarkup = this._dom._aMarkup, nEndTagIndex = this._endTagIndex();
		// getter
		if (!strHTML) {
			var aInnerText = [];
			for(var nTag = this._nStartTagIndex; nTag < nEndTagIndex; ++nTag) {
				aInnerText.push(aMarkup[nTag][this._END_BRACKET_AND_TAIL].substr(1));
			}
			return aInnerText.join('');
		}
		// setter
		else {
			if (nEndTagIndex > this._nStartTagIndex + 1)
				throw new Error('Not Yet Supported: setting innerText when there are nested elements');
			aMarkup[this._nStartTagIndex][this._END_BRACKET_AND_TAIL] = '>' + strHTML;
			return strHTML;
		}
	},
	outerHTML: function() {
		var aMarkup = this._dom._aMarkup, nEndTagIndex = this._endTagIndex();
		if (this._nStartTagIndex != 0 || nEndTagIndex != aMarkup.length - 1)
			aMarkup = aMarkup.slice(this._nStartTagIndex, nEndTagIndex + 1);
		var aResult = [];
		for (var nIndex = 0, length = aMarkup.length; nIndex < length; ++nIndex) {
			var aArr = aMarkup[nIndex];
			// I tried this push(x, x, x, x) method as an optimization of the .concat.apply() method (above)
			// but the performance swings back and forth, I can't tell which is faster
			aResult.push(aArr[0], aArr[1], aArr[2], aArr[3], aArr[4], aArr[5]);
		}
		return aResult.join("");
		//return [].concat.apply([], aMarkup).join("");
	},
	// NOTE: returns matching descendants, not the element itself, in document order
	getElementsByTagName: function(strTag) {
		var bAll = strTag == "*";
		var dom = this._dom, aResults = [], aMarkup = this._dom._aMarkup;
		for (var nIndex = this._nStartTagIndex + 1, length = this._endTagIndex(); nIndex < length; ++nIndex) {
			var aElem = aMarkup[nIndex];
			if (!aElem[this._START_SLASH] && (bAll || aElem[this._TAG_NAME] == strTag))
			aResults.push(new LightningDOMNode(dom, nIndex));
		}
		return aResults;
	},
	parentNode: function() {
		// we're at the top, no parents above us...
		if (this._nStartTagIndex == 0) return null;
		var aMarkup = this._dom._aMarkup;
		var nIndex = this._nStartTagIndex - 1;
		
		// while it's an end-tag, take the partner & go one further up -- if it's self-contained, just go one further up
		while (aMarkup[nIndex][this._START_SLASH] || aMarkup[nIndex][this._END_SLASH]) {
			if (aMarkup[nIndex][this._START_SLASH])
				nIndex = this._findPartnerTag(aMarkup, nIndex, false) - 1;
			else
				nIndex = nIndex - 1;
		}

		return new LightningDOMNode(this._dom, nIndex);
	},
	appendChild: function(elem) {
		var nInsertedTags = 0;
		if (elem._dom == this._dom) throw new Error('Not yet supported: appending a node already in this LightningDOM. Try appending a clone, via .cloneNode()');
		
		// create end-tag if it was self-closing (moving the tail to the end-tag)
		if (this._aTag[this._END_SLASH]) this._addEndTag();
				
		// TODO: remove it via splice() from its old DOM/cache? this way you could MOVE nodes from one dom to another or move nodes around in a DOM!!
		
		// grab all the arrays we want via slice(), insert them with splice()
		var aArgs = elem._dom._aMarkup.slice(elem._nStartTagIndex, elem._endTagIndex() + 1);
		nInsertedTags += aArgs.length;
		var nInsertionPoint = this._endTagIndex();
		// NOTE: the docs say unshift returns the new array. They're wrong, it returns undefined and mutates the array
		aArgs.unshift(nInsertionPoint, 0); // first & second args
		this._dom._aMarkup.splice.apply(this._dom._aMarkup, aArgs); // splice them in at the right place
		
		// reset all object cached properties -- can't we just run initialize()?
		elem._dom = this._dom;
		elem._nStartTagIndex = nInsertionPoint;
		elem._nEndTagIndex = elem._nStartTagIndex + nInsertedTags - 1;
		elem._aTag = elem._dom._aMarkup[elem._nStartTagIndex];
		
		// correct our cached nEndTag position
		// we can't correct caches of other elements b/c we can't get references to them
		// this is BY DESIGN b/c this is a lightweight, generative system
		if (this._nEndTagIndex)
			this._nEndTagIndex += nInsertedTags;
	},
	removeChild: function(elem) {
		if (!elem._dom == this._dom) throw new Error("element passed to removeChild is not part of the same DOM tree");
		var nStartTag = elem._nStartTagIndex;
		var nEndTag = elem._endTagIndex();
		if (!(nStartTag > this._nStartTagIndex && nEndTag < this._endTagIndex())) throw new Error("element passed to elem.removeChild() is not a child of elem");
		var nRemovedTags = nEndTag - nStartTag + 1;
		
		// correct our cached nEndTag position
		// can't do this for other elements b/c we can't get references to them
		// this is BY DESIGN b/c this is a lightweight, generative system
		if (this._nEndTagIndex)
			this._nEndTagIndex -= nRemovedTags;
		
		// actually remove from the array
		this._dom._aMarkup.splice(nStartTag, nRemovedTags);
	},
	// since we don't cache objects, we need some way to tell if 2 objects are referring to the same tag in the array
	is: function(elem) {
		return elem._dom == this._dom && elem._nStartTagIndex == this._nStartTagIndex;
	},
	_addEndTag: function() {
		var aElem = ["<", "/", this._aTag[this._TAG_NAME], "", "", this._aTag[this._END_BRACKET_AND_TAIL]];
		this._aTag[this._END_SLASH] = "";
		this._aTag[this._END_BRACKET_AND_TAIL] = ">";
		this._dom._aMarkup.splice(this._nStartTagIndex + 1, 0, aElem);
		if (this._nEndTagIndex)
			++this._nEndTagIndex;
	},
	// points to the "</DIV>" entry in the this._dom array
	_endTagIndex: function() {
		if (this._nEndTagIndex) return this._nEndTagIndex; // cached
		if (this._aTag[this._END_SLASH]) return this._nStartTagIndex; // self-closing
		return this._nEndTagIndex = this._findPartnerTag(this._dom._aMarkup, this._nStartTagIndex, true);
	},
	_findPartnerTag: function(aMarkup, nIndex, bForward) {
		var aTag = aMarkup[nIndex];
		var aStack = [aTag[this._TAG_NAME]];
		var nDirection = bForward ? 1 : -1;
		var strTagFirst = bForward ? "start" : "end";
		var strTagLast = bForward ? "end" : "start";
		
		// walk forward through the tags until the stack's empty!
		for (nIndex = nIndex + nDirection; aStack.length > 0; nIndex = nIndex + nDirection) {
			aTag = aMarkup[nIndex];
			// skip self-closing tags, push start-tags onto stack, pop end-tags off stack
			if (!aTag[this._END_SLASH]) {
				if (bForward ? !aTag[this._START_SLASH] : aTag[this._START_SLASH])
					aStack.push(aTag[this._TAG_NAME]);
				else {
					var strTagName = aStack.pop();
					if (strTagName != aTag[this._TAG_NAME]) throw new Error("HTML tag mismatch, " + strTagFirst + "-tag '" + strTagName + "' doesn't match " + strTagLast + "-tag '" + aTag[this._TAG_NAME] + "'");
				}
			}
		}
		return nIndex - nDirection; // back off one, we went too far
	}
});

var LightningDOM = Class.create(LightningDOMNode, {
	// override LightningDOMNode's constructor and call it at the end
	initialize: function($super, strMarkup) {
		this._aMarkup = this.parseMarkup(strMarkup);
		$super(this, 0);
	},
	getNode: function(nStartTagIndex) {
		return new LightningDOMNode(this, nStartTagIndex);
	},
	parseMarkup: function(strMarkup) {
		strMarkup = strMarkup.replace(/^\s+/, ''); // strip beginning whitespace
		var aMarkup = [];

		// parse all markup, putting each tag in a separate array element
		// record # matched chars as we go b/c we drop whitespace in two places
		// (btwn "</" and the tagname and btwn attributes and the "/>")
		var match, nMatchedChars = 0;
		
		if (Prototype.Browser.IE) {
			var reTag = new RegExp().compile(/* start tag */ "<(/?)\\s*" + /* tag name*/ "([a-z]+)" + /* attributes */ "((?:\\s*[a-z]+=(?:" + /* double-quoted values */ '(?:\\\\"[^\\\\]*\\\\")|' + /* single-quoted values */ "(?:'[^']*')|" + /* non-quoted values */ "[a-z0-9\\._:-]*" + "))*)?" + /* end tag */ "\\s*(/?)(>[^<]*)", "gi");
			
			// 'sanitize' by escaping double-quotes and removing newlines
			strMarkup = strMarkup.replace(/"/g, '\\"');
			strMarkup = strMarkup.replace(/\r\n/g, '');
			var json = strMarkup.replace(reTag, ',["<", "$1", "$2", "$3", "$4", "$5"]');
			aMarkup = eval('[' + json.substring(1) + ']');
		}
		else {
			var reTag = new RegExp().compile(/* start tag */ "<(/?)\\s*" + /* tag name*/ "([a-z]+)" + /* attributes */ "((?:\\s*[a-z]+=(?:" + /* double-quoted values */ '(?:"[^"]*")|' + /* single-quoted values */ "(?:'[^']*')|" + /* non-quoted values */ "[a-z0-9\\._:-]*" + "))*)?" + /* end tag */ "\\s*(/?)(>[^<]*)", "gi");
			while(match = reTag.exec(strMarkup)) {
				nMatchedChars += match[0].length;
				aMarkup.push(["<", match[1], match[2], match[3], match[4], match[5]]); //.join("")
			}

			// ensure that our markup consumed the whole string (minus leading whitespace)
			// TODO: add this test to the IE branch, make this test more efficient by adding up the length
			if (nMatchedChars != strMarkup.length)
				alert("Failed to parse all the markup. Parsed " + nMatchedChars + " out of " + strMarkup.length + ". Here's what parsed: " + aMarkup.join(""));
		}
		

		// IEBUG: IE REMOVES self-closing marks on <INPUT /> tags -- add them back & return true
		// TODO: get a list of all elements IE does this for -- and only do this step if it's IE...
		for (var nIndex = 0, length = aMarkup.length; nIndex < length; ++nIndex) {
			var aTag = aMarkup[nIndex];
			if (aTag[this._TAG_NAME] == "INPUT")
				aTag[this._END_SLASH] = "/";
		}

		return aMarkup;
	},
	// OVERRIDE endTagIndex()
	// b/c the cache can get out-of-sync w/ LightningDOMNodes and we don't want the LightningDOM to ever be out of sync
	_endTagIndex: function() {
		return this._aMarkup.length - 1;
	}
});