// =============================================================================
// XDOM (DOM is operated in the XPath style)
// (c) 2007 shiz
// web site: http://js.tank.jp/
// license : MIT-style license
// version 0.1.0 (2007-05-07)
// =============================================================================

/**
 * DOM is operated in the XPath style.
 * @class XDOM
 * @constructor
 * @param {HTTPRequest | requestXML | Node | [Node]} node
 * If it is HTTPRequest or requestXML, current is "#document".
 */
XDOM = function(node){
	if (node){
		var current;
		var flagAdjustCurrent = false;
		// CURRENT NODE
		if (node.responseXML && node.responseXML.documentElement){
			current = node.responseXML.documentElement;
			flagAdjustCurrent = true;
		}else if (node.documentElement){
			current = node.documentElement;
			flagAdjustCurrent = true;
		}else{
			current = node;
		}
		if (flagAdjustCurrent && current.parentNode && current.parentNode.nodeName == '#document'){
			current = current.parentNode;
		}
		// ROOT NODE
		var root = (current.length) ? current[0] : current;
		for (;root.parentNode;){
			root = root.parentNode;
		}
	}
	this.$current = current ? current : null;
	this.$parser = new XDOM.Parser(root ? root : null);
};

XDOM.prototype = {
	/**
	 * get new XDOM (by using XPath)
	 * @method xpath
	 * @param {String} xpath XPath expression
	 * @return {XDOM}
	 */
	xpath: function(xpath){
		return new XDOM(this.$parser.parse(this.$current, xpath));
	},
	/**
	 * move current (by using XPath)
	 * @method move
	 * @param {String} xpath XPath expression
	 * @return {Bool}
	 */
	moveCurrent: function(xpath){
		this.$current = this.$parser.parse(this.$current, xpath);
		return this.$current ? true : false;
	},
	/**
	 * return oneself (XDOM)
	 * @method self
	 * @param {Number} num using when there are two or more current node.(can omit)
	 * @return {XDOM}
	 */
	self: function(num){
		if (num >= 0){
			var node = this.$current[num] ? this.$current[num] : this.$current;
			return new XDOM(node);
		}else{
			return this;
		}
	},
	/**
	 * get current node
	 * @method node
	 * @param {Number} num using when there are two or more current node.(can omit)
	 * @return {Node | [Node]}
	 */
	node: function(num){
		var node = (num >= 0) ? this.$current[num] : this.$current;
		return node;
	},
	/**
	 * get current node length
	 * @method length
	 * @return {Number}
	 */
	length: function(){
		return this.$current ? (this.$current.length ? this.$current.length : 1) : 0;
	},
	/**
	 * get current node's text
	 * @method text
	 * @param {Number} num using when there are two or more current node.(can omit)
	 * @return {String | [String]}
	 */
	text: function(num){
		var node = (num >= 0) ? this.$current[num] : this.$current;
		return this.$parser.text(node);
	},
	/**
	 * get current node's attributes
	 * @method attr
	 * @param {String} name It narrows it by the Attribute's name.(can omit)
	 * @param {Number} num using when there are two or more current node.(can omit)
	 * @return {String | [String]}
	 */
	attr: function(name, num){
		var node = (num >= 0) ? this.$current[num] : this.$current;
		return name ? this.$parser.attribute(node,name) : this.$parser.attribute(node,'*');
	}
};
// like XSL
XDOM.prototype.match = XDOM.prototype.moveCurrent;
XDOM.prototype.select = XDOM.prototype.xpath;

// Sub Routine
(function(){
var util = {
	$expr: {
		'position\\([\\s\\n\\t\\r]*\\)': '(i+1)',
		'text\\([\\s\\n\\t\\r]+\\)': 'this.text(n[i])',
		'first\\([\\s\\n\\t\\r]+\\)': '(i==0)',
		'last\\([\\s\\n\\t\\r]+\\)': '(i==len-1)',
		'true\\([\\s\\n\\t\\r]+\\)': '(true)',
		'false\\([\\s\\n\\t\\r]+\\)': '(false)'
	},
	$operator: {
		'[\\s\\n\\t\\r]+and[\\s\\n\\t\\r]+': '&&',
		'[\\s\\n\\t\\r]+or[\\s\\n\\t\\r]+': '||'
	},
	isDigits: function(path){
		return path.match(/^[0-9]+$/) ? true : false;
	},
	trim: function(str){
		return str.replace(/^[\s\n\t\r]+|[\s\n\t\r]+$/g, '');
	},
	removeLiteral: function(str){
		return str.replace(/[\"\'][^\"\']*[\"\']/g, '');
	},
	divXPath: function(xpath){
		var obj = {left: xpath, right: ''};
		var m = xpath.match(/[^\/\[\]]+(\[([^\[\]])+\])*[\s\n\t\r]*/);
		if (m && m[0]){
			obj.left = m[0];
			if (xpath.charAt(m[0].length) == '/'){
				obj.right = xpath.substring(m[0].length,xpath.length);
			}
		}
//		alert('left : ' + obj.left + '\nright: ' + obj.right); // debug
		return obj;
	},
	divStep: function(step){
		var step2 = this.trim(step);
		var obj = {};
		
		if (step2.match(/^.+\[.+\]$/)){
			obj.expr = step2.substring(step2.indexOf('[')+1,step2.indexOf(']'));
			step2 = step2.substring(0,step2.indexOf('['));
		}else{
			obj.expr = '';
		}
		step2 = this.trim(step2);
		if (step2.match(/^[^:]+::[^:\[]+/)){
			obj.axis = step2.substring(0,step2.indexOf('::'));
			obj.name = step2.substring(step2.indexOf('::')+2,step2.length);
		}else{
			obj.axis = '';
			obj.name = step2;
		}
		obj.axis = this.trim(obj.axis);
		obj.name = this.trim(obj.name);
		obj.expr = this.trim(obj.expr);
//		alert('axis: '+obj.axis+'\nname: '+obj.name+'\nexpr:'+obj.expr);// debug
		
		return obj;
	},
	repExpr: function(expr){
		var e = this.trim(expr);
		if (!e){return '';}
		var e2 = this.removeLiteral(e);
		for (var reg in this.$operator){
			var m = e2.match(new RegExp(reg, 'g'));
			if (m){
				for (var i = 0,len = m.length; i < len; i++){
					e = e.replace(m[i], this.$operator[reg]);
				}
			}
		}
		// =
		var e2 = this.removeLiteral(e);
		var m = e.match(/[^\!<>=\s\n\t\r]+=[^\!<>=\s\n\t\r]+/g);
		if (m){
			for (var i = 0,len = m.length; i < len;i++){
				e = e.replace(m[i], m[i].replace('=', '=='));
			}
		}
		
		var result = '';
		var m = e.match(/[^&]+/g);
		if (m){
			for (var i = 0,len = m.length; i < len; i++){
				var m2 = m[i].match(/[^\|]+/g);
				if (m2 && m2.length > 0){
					for (var j = 0, len2 = m2.length; j < len2; j++){
						result += this._repExpr(m2[j]);
						result += (j < len2-1) ? '||' : '';
					}
				}else{
					result += this._repExpr(m[i]);
				}
				result += (i < len-1) ? '&&' : '';
			}
		}
		
//		alert(result); // debug
		return result;
	},
	_repExpr: function(expr){
		var e = this.trim(expr);
		// digits only
		if (util.isDigits(e)){
			e = 'position()=='+e;
		}
		// @
		var m = e.match(/@[^\!<>=\[\]:]+/g);
		if (m){
			for (var i = 0,len = m.length; i < len;i++){
				e = e.replace(m[i], m[i].replace('@', 'this.attribute(n[i],"')+'")');
			}
		}
		// xpath
		var e2 = this.removeLiteral(e);
		var m = e2.match(/[^\!<>=\"\&\|']+/g);
		if (m){
			for (var i = 0,len = m.length; i < len;i++){
				m[i] = this.trim(m[i]);
				if (!(util.isDigits(m[i]) || m[i].match(/[\(\)]/))){
					e = e.replace(m[i], 'this.parse(n[i],"'+m[i]+'")');
				}
			}
		}
		// other
		for (var reg in this.$expr){
			var re = new RegExp(reg, 'g');
			e = e.replace(re, this.$expr[reg]);
		}
		
		// xpath+operator
		var m = e.match(/this.parse\([^\(\)\"]+\"[^\"]*\"\)[\s\n\t\r]*(==|\!=|>|<|>=|<=)[\s\n\t\r]*[\"|\'][^\"\']*[\"|\']/g);
		if (m){
			for (var i = 0,len = m.length; i < len;i++){
				var m2 = e.match(/[\s\n\t\r]*(==|\!=|>|<|>=|<=)/g);
				var e3 = m[i].replace(/\)[\s\n\t\r]*(==|\!=|>|<|>=|<=)/, '),');
				e = e.replace(m[i], 'this.existText('+e3+',"'+m2[0]+'")');
			}
		}
		return e;
	},
	repXPath: function(path){
		var path2 = path.replace(/\/\//g,'/descendant-or-self::node()/');
		path2 = path2.replace(/\.\./g,'parent::node()');
		path2 = path2.replace(/\./g,'self::node()');
		path2 = path2.replace(/node\(\)/g,'*');
		path2 = path2.replace(/^\//,'');
		path2 = path2.replace(/\/$/,'');
		
		return path2;
	}
};
//XDOM.util = util; // debug

// XDOM Parser
XDOM.Parser = function(root){
	this.$root = root;
};

XDOM.Parser.prototype = {
	parse: function(node, path){
		if (!node){return null;}
		var path2 = util.trim(path ? path : '');
		if (path2 == '/' || !path2){
			return this.$root;
		}
		var nodes = path2.match(/^\//) ? this.$root : node;
		nodes = this._parse(nodes, path2);
		return nodes.length ? ((nodes.length == 1) ? nodes[0] : nodes) : null;
	},
	_parse: function(node, path){
		if (!node){return [];}
		var xpath = util.divXPath(util.repXPath(path));
		if (!xpath.left){return node;}
		var nodes = node.length ? node : [node];
		
		var result = [];
		var step = util.divStep(xpath.left);
		switch (step.axis){
		case 'child':
		case '':
			result = this.child(nodes,step.name,step.expr);
			break;
		case 'descendant':
			result = this.descendant(nodes,step.name,step.expr);
			break;
		case 'descendant-or-self':
			result = this.descendantOrSelf(nodes,step.name,step.expr);
			break;
		case 'parent':
			result = this.parent(nodes);
			break;
		case 'self':
			result = this.self(nodes,step.name,step.expr);
			break;
		default:
			return [];
		}
		if (xpath.right){
			var result = this._parse(result, xpath.right);
		}
		return result ? result : [];
	},
	self: function(node, name, expr){
		var nodes = this.evalNode(node, name);
		nodes = expr ? this.evalExpr(nodes, expr) : nodes;
		
		return (nodes.length > 0) ? nodes : null;
	},
	child: function(node, name, expr){
		var nodes = [];
		for (var i = 0,len = node.length; i < len; i++){
			if (node[i] && node[i].childNodes){
				nodes = nodes.concat(this.evalNode(node[i].childNodes, name));
			}
		}
		nodes = expr ? this.evalExpr(nodes, expr) : nodes;
		
		return (nodes.length > 0) ? nodes : null;
	},
	parent: function(node){
		var parent = [];
		
		for (var i = 0,len = node.length; i < len; i++){
			if (node[i] && node[i].parentNode){
				parent.push(node[i].parentNode);
			}
		}
		
		return (parent.length > 0) ? parent : null;
	},
	descendant: function(node, name, expr){
		return this.child(node, name, expr);
	},
	descendantOrSelf: function(node, name, expr){
		return this.descendant(node, name, expr) || this.self(node, name, expr);
	},
	evalNode: function(node, name){
		var result = [];
		if (!node){return result;}
		
		for (var i = 0,len = node.length; i < len; i++){
			if (name == '*' || node[i].nodeName == name){
				result.push(node[i]);
			}
		}
		
		return result;
	},
	evalExpr: function(node, expr){
		var n = node;
		e = util.repExpr(expr);
		var result = [];
		
		for (var i = 0,len = n.length; i < len; i++){
			try{
				if (eval(e)){
					result.push(n[i]);
				}
			}catch(error){
//				alert(error.name+':'+error.message); // debug
				return [];
			}
		}
		return result;
	},
	attribute: function(node, nodetestName){
		if (!node){return null;}
		var nodes = node.length ? node : [node];
		
		var attributes = [];
		for (var i = 0,len = nodes.length; i < len; i++){
			attributes = attributes.concat(this._attribute(nodes[i], nodetestName));
		}
		return (attributes.length > 0) ? ((attributes.length == 1) ? attributes[0] : attributes) : null;
	},
	_attribute: function(node, nodetestName){
		var result = [];
		if (!node){return result;}
		if (nodetestName == '*' || nodetestName == ''){
			var attr = node.attributes;
			if (attr){
				for (var i = 0,len = attr.length; i < len; i++){
					result.push(attr[i].value);
				}
			}
		}else{
			var attr = node.getAttribute(nodetestName);
			if (attr){
				result.push(attr);
			}
		}
		return result;
	},
	text: function(node){
		if (!node){return null;}
		var nodes = node.length ? node : [node];
		var nodes = this.child(nodes, '*');
		if (!nodes){return null;}
		var text = [];
		
		for (var i = 0,len = nodes.length; i < len; i++){
			if (nodes[i] && nodes[i].nodeValue
				 && nodes[i].nodeName != '#comment' && nodes[i].nodeName != 'xml'){
				text.push(nodes[i].nodeValue);
			}
		}
		return (text.length > 0) ? ((text.length == 1) ? text[0] : text) : null;
	},
	existText: function(node, str, ope){
		if (!node){return null;}
		var nodes = node.length ? node : [node];
		var text = this.text(nodes);
		if (!text){return false;}
		text = (text instanceof Array) ? text : [text];
		for (var i = 0,len = text.length; i < len; i++){
			try{
				if (eval('text[i] '+ope+' str')){
					return true;
				}
			}catch(error){
//				alert(error.name+':'+error.message); // debug
			}
		}
		return false;
	}
}

})();