/**
 * Planned:
 *  Support negative an+b references
 *  :contains("String")
 *
 * Other Ideas
 *----------------------------------
 * + Table DOM issues (sort?)
 * + Create node with attributes?
 * + Upward traversal getParentBy[CSS|TagName|Distance|ClassName]()
 * + yankNode, wrapNode, tidyDOM
 */

NG.dom = function () {};

NG.dom.ELEMENT_NODE                = 1;
NG.dom.ATTRIBUTE_NODE              = 2;
NG.dom.TEXT_NODE                   = 3;
NG.dom.CDATA_SECTION_NODE          = 4;
NG.dom.ENTITY_REFERENCE_NODE       = 5;
NG.dom.ENTITY_NODE                 = 6;
NG.dom.PROCESSING_INSTRUCTION_NODE = 7;
NG.dom.COMMENT_NODE                = 8;
NG.dom.DOCUMENT_NODE               = 9;
NG.dom.DOCUMENT_TYPE_NODE          = 10;
NG.dom.DOCUMENT_FRAGMENT_NODE      = 11;
NG.dom.NOTATION_NODE               = 12;


NG.dom.tokenize = function (str, splits, groups) {
	var tokens=[],stack=[],i,c,buf='';
	if (isArr(splits)) {
		c={};
		for (i=0; i<splits.length; i++) c[splits[i]] = T;
		splits=c;
	}
	for (i=0; i<str.length; i++) {
		c=str.charAt(i);
		if (stack.empty() && splits[c]) { tokens.push(buf); buf=''; }
		if (groups[c] && c != groups[c]) {
			stack.push(groups[c]);
		} else if (!stack.empty()) {
			if (c==stack.tail()) stack.pop();
		}
		if (!tokens.empty() && buf=='' && splits[c]) {
			tokens.push(c);
		} else {
			buf += c;
		}
	}
	tokens.push(buf); return tokens;
}


NG.dom.CSSAttributeMatch = function (str) {
	this.src = N;
	this.attrName = N;
	this.cmp = N;
	this.to = N;
	if (str) {
		this.src = str.replace(/\[|\]/,'');
		var p = this.src.split(/([\|\^\$\*~]?=)/);
		this.attrName = p[0];
		if (p.length > 1) {
			this.cmp = p[1];
			this.to  = p[2];
		}
	}

	this.matches = function (node) {
		if (node.hasAttribute(this.attrName)) {
			if (!this.cmp) return T;
			return this.strcmp(this.to,this.cmp,node.getAttribute(this.attrName));
		}
		return F;
	}
}

NG.dom.CSSAttributeMatch.strcmp = function (a,op,b) {
	var i;
	switch (op) {
		case  '=': return a==b;
		case '|=': return a==b || b.substr(0,a.length+1) == a+'-';
		case '^=': return b.substr(0,a.length) == a;
		case '$=': return b.substr(-a.length) == a;
		case '*=': return b.indexOf(a) != -1;
		case '~=':
			if (a==b) return T;
			if ((i=b.indexOf(a)) < 0) return F;
			if (b.charAt(i-1) != ' ') return F;
			i+=a.length;
			return (i == b.length || b.charAt(i) == ' ');
		default: return N;
	}
}
NG.dom.CSSAttributeMatch.prototype.strcmp = NG.dom.CSSAttributeMatch.strcmp;


NG.dom.CSSPseudoClass = function (str) {
	this.src = N;
    this.type = N;
    this.expr = N;
    this.data = N;

	if (str) {
		this.src = str.trim();
		var p = this.src.match(/^([-a-z]+)(\((.*)\))?$/);
		this.type = p[1];
		if (p[3]) this.expr = p[3];
	}

	this.matches = function (n) {
		var i=1,t;
		if (isNull(this.data)) this.parse();
		switch (this.type) {
			case 'not': return !this.data.matches(n);
			case 'first-child': return (n.previousSibling) ? F : T;
			case 'last-child': return (n.nextSibling) ? F : T;
			case 'only-child': return (n.previousSibling || n.nextSibling) ? F : T;
			case 'empty': return (n.childNodes.length==0) ? T : F;
			case 'first-of-type':
				for (t=n.previousSibling; t; t=t.previousSibling) if (t.tagName==n.tagName) return F;
				return T;
			case 'last-of-type':
				for (var t=n.nextSibling; t; t=t.nextSibling) if (t.tagName==n.tagName) return F;
				return T;
			case 'only-of-type':
				var cn = n.parentNode.childNodes
				for (i=0; i<nodes.length; i++) if (cn[i].tagName && cn[i].tagName==n.tagName) return F;
				return T;
			case 'nth-child':
				for (t=n.previousSibling; t; t=t.previousSibling) if (t.nodeType==NG.dom.ELEMENT_NODE) i++;
				return (this.data.a==0) ? (i==this.data.b) : (i%this.data.a==this.data.b);
			case 'nth-last-child':
				for (t=n.nextSibling; t; t=t.nextSibling) if (t.nodeType==NG.dom.ELEMENT_NODE) i++;
				return (this.data.a==0) ? (i==this.data.b) : (i%this.data.a==this.data.b);
			case 'nth-of-type':
				for (t=n.previousSibling; t; t=t.previousSibling) if (t.tagName==n.tagName) i++;
				return (this.data.a==0) ? (i==this.data.b) : (i%this.data.a==this.data.b);
			case 'nth-last-of-type':
				for (var t=n.nextSibling; t; t=t.nextSibling) if (t.tagName==n.tagName) i++;
				return (this.data.a==0) ? (i==this.data.b) : (i%this.data.a==this.data.b);
			default: return N;
		}
	}

	this.parse = function () {
		var d={};
		switch (this.type) {
			case 'nth-child':
			case 'nth-last-child':
			case 'nth-of-type':
			case 'nth-last-of-type': // an+b
				var p = this.expr.replacev(['odd','even'],['2n+1','2n']);
				p = p.match(/((-?\d+)?(n))?(\+(\d+))?/);
				d.a = parseInt(p[1]); d.b = parseInt(p[4]);
				d.b = isNaN(d.b) ? 0 : d.b;
				d.a = isNaN(d.a) ? (p[3]=='n' ? 1 : 0) : d.a;
				break;
			case 'not':
				d = new NG.dom.CSSSimpleSelector(this.expr);
		}
		this.data=d;
	}
}

NG.dom.CSSSimpleSelector = function (str) {
	this.src = N;
	this.tagName = N;
	this.idAttr = N;
	this.classNames = [];
	this.pseudoClasses = [];
	this.attrMatches = [];
	this.hints = {};

	if (str) {
		this.src = str.trim();
		var p = NG.dom.tokenize(this.src, ['#','.',':','['], {'(':')','[':']','"':'"'});
		for (var i=0; i<p.length; i++) {
			switch (p[i]) {
				case '#': this.idAttr = p[++i]; break;
				case '.': this.classNames.push(p[++i]); break;
				case ':': this.pseudoClasses.push(new NG.dom.CSSPseudoClass(p[++i])); break;
				case '[': this.attrMatches.push(new NG.dom.CSSAttributeMatch(p[++i])); break;
				case '*': break;
				default: if (i==0) this.tagName = p[i].toUpperCase(); break;
			}
		}
	}

	this.setClassNames = function (v) {
		if (isStr(v)) this.classNames = v.split('.');
		if (isArr(v)) this.classNames = v;
	}

	this.matches = function (node) {
		if (node.nodeType != NG.dom.ELEMENT_NODE) return F;
		if (this.idAttr  && this.idAttr  != node.getAttribute('id')   ) return F;
		if (this.tagName && this.tagName != node.tagName.toUpperCase()) return F;
		for (var i=0; this.classNames && i<this.classNames.length; i++)
			if (!NG.dom.CSSAttributeMatch.strcmp(this.classNames[i],'~=',node.className)) return F;
		for (var i=0; this.attrMatches && i<this.attrMatches.length; i++)
			if (!this.attrMatches[i].matches(node)) return F;
		for (var i=0; this.pseudoClasses && i<this.pseudoClasses.length; i++)
			if (!this.pseudoClasses[i].matches(node)) return F;
		return T;
	}
}

function getElementsByClassName(node,className) {
	var nodelist=[],ss;
	if (isArr(className, NG.dom.CSSSimpleSelector)) ss = className;
	else (ss = new NG.dom.CSSSimpleSelector()).setClassNames(className);

	for (var i=0; node.childNodes && i<node.childNodes.length; i++) {
		if (ss.matches(node.childNodes[i])) nodelist.push(node.childNodes[i]);
		nodelist = nodelist.concat(getElementsByClassName(node.childNodes[i], className));
	}
	return nodelist;
}

function isChildOf(a,b) {
	var x=a;
	if (!x || !x.parentNode) return false;
	do { x = x.parentNode; } while (x!=b);
	return x==b ? true : false;
}

function getElementsByCSS(node,selector) {
	var i,j,c,t,tmp,e,ssel,ssels=[],combs=[],nodes=[node],newnodes=[];
	var tokens = NG.dom.tokenize(selector.trim(), [' ','+','>','<','~'], {'[':']','(':')','"':'"'});
	if (![' ','+','>','<','~'].contains(tokens[0])) tokens.unshift(' ');

	for (i=0; i<tokens.length; i+=2) {
		combs.push(tokens[i]);
		ssels.push(tokens[i+1]);
	}

	for (i=0; i<ssels.length; i++) {
		ssel = new NG.dom.CSSSimpleSelector(ssels[i]);
		for (j=0; j<nodes.length; j++) {
			switch (combs[i]) {
				case '>': // Direct Descendants (1st generation)
					for (c=0; c<nodes[j].childNodes.length; c++) {
						if (ssel.matches(nodes[j].childNodes[c])) {
							newnodes.push(nodes[j].childNodes[c]);
						}
					}
					break;
				case '+':
					for (c=nodes[j].nextSibling; c && c.nodeType != NG.dom.ELEMENT_NODE; c=c.nextSibling)
					if (c && ssel.matches(c)) newnodes.push(c);
					break;
				case '~':
					for (c=nodes[j].nextSibling; c; c = c.nextSibling) if (ssel.matches(c)) newnodes.push(c);
					break;
				case '<':
					if (ssel.matches(nodes[j].parentNode)) newnodes.push(nodes[j].parentNode);
					break;
				case '-':
					for (c=nodes[j].previousSibling; c && c.nodeType != NG.dom.ELEMENT_NODE; c = c.previousSibling);
					if (c && ssel.matches(c)) newnodes.push(c);
					break;
				case ' ':
					if (ssel.idAttr) {
						if (tmp=document.getElementById(ssel.idAttr)) {
							if (ssel.matches(tmp) && isChildOf(tmp, nodes[j])) newnodes.push(tmp);
						}
					} else if (ssel.tagName) {
						tmp = nodes[j].getElementsByTagName(ssel.tagName);
						for (c=0; c<tmp.length; c++) if (ssel.matches(tmp[c])) newnodes.push(tmp[c]);
					} else if (ssel.classNames) {
						tmp = getElementsByClassName(nodes[j], ssel.classNames);
						for (c=0; c<tmp.length; c++) if (ssel.matches(tmp[c])) newnodes.push(tmp[c]);
					} else if (i == frags.length -1) {
						// TODO: This should probably be everything _below_ this level + run matches
						for (c=0; c<nodes[j].childNodes.length; c++) {
							if (nodes[j].childNodes[c].nodeType == NG.dom.ELEMENT_NODE) {
								newnodes.push(nodes[j].childNodes[c]);
							}
						}
					}
			}
		}
		nodes=newnodes; newnodes=[];
	}
	return nodes;
}