/*
	This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
	
	Author: Sam Tsvilik
	Version: 4.0 Beta
	Last Modified: 08/29/2009
*/
(function($) {
	//Converts XML DOM to JSON				
	//Helper functions
	var util = {
		isIE: function(){ return (+"\0"===0); },
		isStr: function(o) { return typeof(o) === "string"; },
		isFn: function(o) { return typeof(o) === "function"; },
		isDef: function(o) {return (typeof(o) !== "undefined");	},
		isArr: function(o) { return o instanceof Array; },
		isInt: function(o) { return o instanceof Number && !isNaN(o); },
		isNum: function(s) {
			var out = false;					
			if(util.isStr(s)) { 
				var pattern = /^((-)?([0-9]*)((\.{0,1})([0-9]+))?$)/;
				out = pattern.test(s);
			}
			return out;
		},
		isXNode: function(o) { return (typeof(o) === "object" && this.isDef(o.nodeName)); },
		isNodeSet: function(o) { return o instanceof INodeSet; },
		//Alters attribute and collection names to comply with JS
		trim: function(str) {
			var out = str;
			if(this.isStr(str)) {
				out = str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
			}
			return out;
		},
		formatName: function(name) {					
			var tName = this.trim(String(name));
			return tName;
		}
	};
	
	var XMLSerializer = {
		//Creates an XMLDomDocument instance
		newDocument: function(rootTagName, namespaceURL) {
		  if (!rootTagName) rootTagName = "";
		  if (!namespaceURL) namespaceURL = "";
		  if (document.implementation && document.implementation.createDocument) {
			// This is the W3C standard way to do it
			return document.implementation.createDocument(namespaceURL, rootTagName, null);
		  } else { // This is the IE way to do it
			// Create an empty document as an ActiveX object
			// If there is no root element, this is all we have to do
			var doc = new ActiveXObject("MSXML2.DOMDocument");
			// If there is a root tag, initialize the document
			if (rootTagName) {
			  // Look for a namespace prefix
			  var prefix = "";
			  var tagname = rootTagName;
			  var p = rootTagName.indexOf(':');
			  if (p !== -1) {
				prefix = rootTagName.substring(0, p);
				tagname = rootTagName.substring(p+1);
			  }
			  // If we have a namespace, we must have a namespace prefix
			  // If we don't have a namespace, we discard any prefix
			  if (namespaceURL) {
				if (!prefix) prefix = "a0"; // What Firefox uses
			  } else prefix = "";
			  // Create the root element (with optional namespace) as a
			  // string of text
			  var text = "<" + (prefix?(prefix+":"):"") +  tagname +
				  (namespaceURL
				   ?(" xmlns:" + prefix + '="' + namespaceURL +'"')
				   :"") +
				  "/>";
			  // And parse that text into the empty document
			  doc.loadXML(text);
			}
			return doc;
		  }
		},
		//Recursively converts JSON object to an XML node
		objToNode: function(curNode, obj) {
			var subElement, val, newNode;
			if(util.isDef(curNode) && util.isDef(obj)) {
				for(subElement in obj) {
					if(obj.hasOwnProperty && obj.hasOwnProperty(subElement)) {
						val = obj[subElement];
						if(subElement.indexOf("@") !== -1) {
							curNode.setAttribute(subElement.replace('@',''), val);
						} else if(subElement === "$comments") {
							if(!!val.length) {
								var c = 0, clen = val.length-1, cc;									
								do {
									cc = val[c];
									curNode.appendChild(this.createComment(cc));
								} while(c++ < clen); 
							}
						} else if(util.isNodeSet(val)) {
							var n = 0, nlen = val.length-1, nn;
							do {
								nn = val[n];
								if(!!nn.ns) {
									if(util.isDef(this.createElementNS)) {
										newNode = curNode.appendChild(this.createElementNS(nn.ns, subElement));
									} else {										
										newNode = curNode.appendChild(this.createNode(1, subElement, nn.ns));
									}
								} else {
									newNode = curNode.appendChild(this.createElement(subElement));
								}								
								if(!!nn.Text) {	newNode.appendChild(util.isDef(nn.hasCDATA)?this.createCDATASection(nn.Text):this.createTextNode(nn.Text)); }
								XMLSerializer.objToNode.call(this, newNode, nn);
							} while(n++ < nlen);																		
						}
					}
				}
			}
		}
	};
	//Base child element
	var IChild = function(parent) {
		this.parent = parent || null;
	};
	//Root Node Class
	var IRoot = function(name) {				
		this.nodeName = name || "";
		this.ns = "";				
	};	
	//Node Class
	var INode = function() {
		var parent = null, name = null, value = null;
		switch(arguments.length) {
			case 1: name = arguments[0]; break;
			case 2: parent = arguments[0]; name = arguments[1]; break;
			case 3: parent = arguments[0]; name = arguments[1]; value = arguments[2]; break;
			default: name = "noname";
		}
		IRoot.apply(this, [name]);
		IChild.apply(this, [parent]);
		this.Text = value || "";				
	};	
	//Node Object - can have children
	var INodeSet = function() {
		this.length = 0;
		this.push = function(elem){ Array.prototype.push.call( this, elem ); };
		this.sort = function() { Array.prototype.sort.apply( this, arguments ); };
	};
	INodeSet.prototype = {
		getNodesByAttribute: function(attr, obj){
			var out = [];
			if(!!this.length && util.isStr(attr) && util.isDef(obj)) {
				var n = this.length, node, aval;
				while(n--) {
					node = this[n]; aval = node[attr];
					if(util.isDef(aval) && aval === obj) { out.unshift(node); }
				}
			}
			return (!out.length)?null:out;
		},
		getNodesByValue: function(obj){
			var out = [];
			if(!!this.length && util.isDef(obj)) {
				var n = this.length, node;
				while(n--) {
					node = this[n];
					if(node.Text === obj) { out.unshift(node); }
				}
			}
			return (!out.length)?null:out;
		},
		contains: function(attr, obj){
			var out = false;
			if(!!this.length && util.isStr(attr) && util.isDef(obj)) {
				var n = this.length, node, aval;
				while(n--) {
					node = this[n]; aval = node[attr];
					if(util.isDef(aval) && aval === obj) { out = true; break; }
				}
			}
			return out;
		},
		indexOf: function(){
			var out = -1, attr, obj;
			if(!!this.length) {
				var n = this.length, node, val;
				switch(arguments.length) {
					case 1: obj = arguments[0];
						while(n--) {
							node = this[n]; val = node.val();
							if(util.isDef(val) && val === obj) { out = n; break; }
						}
						break;
					case 2: attr = arguments[0]; obj = arguments[1];
						while(n--) {
							node = this[n]; val = node[attr];
							if(util.isDef(val) && val === obj) { out = n; break; }
						}
						break;
				}						
			}
			return out;	
		},
		sortByAttribute: function(attr, dir){
			if(!!this.length && util.isStr(attr)) {
				this.sort(function(a,b) {
					var _a = (a.attr(attr)), 
						_b = (b.attr(attr));
					_a = util.isNum(_a)?parseFloat(_a):_a;
					_b = util.isNum(_b)?parseFloat(_b):_b;
					var _out = (_a<_b)?-1:(_b<_a)?1:0;
						_out = (util.isDef(dir) && dir.toLowerCase() === "desc")?(0-_out):_out;
					return _out;
				});
			}
		},	
		sortByValue: function(dir){
			if(!!this.length) {
				this.sort(function(a,b) {
					var _a = (a.Text), 
						_b = (b.Text);
					_a = util.isNum(_a)?parseFloat(_a):_a;
					_b = util.isNum(_b)?parseFloat(_b):_b;
					var _out = (_a<_b)?-1:(_b<_a)?1:0;
						_out = (util.isDef(dir) && dir.toLowerCase() === "desc")?(0-_out):_out;
					return _out;
				});
			}	
		},
		sortByChildNode: function(nodeName, dir){
			if(!!this.length && util.isStr(nodeName)) {
				this.sort(function(a,b) {
					var _a = a[nodeName],
						_b = b[nodeName];
					_a = (util.isDef(_a) && !!_a.length)?_a[0].Text:null;
					_b = (util.isDef(_b) && !!_b.length)?_b[0].Text:null;
					//---------------------------------------------------//
					_a = util.isNum(_a)?parseFloat(_a):_a;
					_b = util.isNum(_b)?parseFloat(_b):_b;
					var _out = (_a<_b)?-1:(_b<_a)?1:0;
						_out = (util.isDef(dir) && dir.toLowerCase() === "desc")?(0-_out):_out;
					return _out;
				});
			}	
		},	
		first: function() {
			var out = null;
			if(!!this.length) { out = this[0]; }
			return out;
		},
		last: function() {
			var out = null;
			if(!!this.length) { out = this[this.length-1]; }
			return out;
		}	
	};
	//Engine Factory
	var XMLObjectifierEngine = (function() {				
		var _public = {
			makeNodeSet: function() {
				var node = new INodeSet();				
				return node;
			},
			makeNode: function(parent, obj)	{
				var name = obj.localName||obj.baseName;
					name = util.formatName(name);
				var node = new INode(parent, name);
					node.ns = obj.prefix || "";							
					this.setAttributes(node, obj);
				return node;
			},
			setAttributes: function(obj, xnode) {
				if(util.isDef(xnode) && !!xnode.attributes.length) {							
					var a = xnode.attributes.length-1, attName = null, objParent = null;
					do { //Order is irrelevant (speed-up)
						attName = "@"+xnode.attributes[a].name;							
						obj.attr(attName, xnode.attributes[a].value);
					} while(a--);
				}
			},
			run: function(parent, xobj) {
				var curChild, newNode;
				if(util.isDef(parent) && util.isDef(xobj)) {
					if(xobj.hasChildNodes()) {
						var nodesLen = xobj.childNodes.length-1, n = 0;
						do {
							curChild = xobj.childNodes[n];
							switch(curChild.nodeType) {
								case 1: //Node										
									//Create a single node
									newNode = _public.makeNode(parent, curChild);
									if(util.isFn(this.decorator)) {
										var _dout = this.decorator.call(newNode);
										//Skip node if decorator returns false
										if(_dout === false) { continue; }
									}
									if(!!newNode) {
										//Add child node to parent
										parent.appendChild(newNode);
										//Recursive call if node contains children
										if(curChild.hasChildNodes()){ _public.run.apply(this, [newNode, curChild]); }												
									}
									break;
								case 3: //Text Value
									parent.val(util.trim(curChild.nodeValue));	
									break;
								case 4: //CDATA										
									parent.hasCDATA = true;
									parent.val(util.isDef(curChild.text)?util.trim(curChild.text):util.trim(curChild.nodeValue));												
									break;
								case 8: //Comments
									if(!util.isDef(this.noComments) || !this.noComments) {
										if(!util.isDef(parent.$comments)) { parent.$comments = []; }
										parent.$comments.push(util.trim(curChild.nodeValue));
									}
									break;											
							}
						} while(n++ < nodesLen);
					}
				}
			},
			init: function(xobj, opt) {
				opt = opt || {noComments: true};
				if(util.isStr(xobj)){
					xobj = this.textToXML(xobj);
				} else if(util.isXNode(xobj)){ xobj = xobj;
				} else { xobj = null; }
				//If invalid xml object - abort
				if(!xobj){ return null; }
				//Determine a document root node
				var xroot = (xobj.nodeType === 9)?xobj.documentElement:(xobj.nodeType === 11)?xobj.firstChild:xobj;
				//Root Node
				var root = new IRoot(xroot.nodeName);
				if(util.isFn(opt.decorator)) {
					opt.decorator.call(root);
				}
				//If init argument is just a text or CDATA return value
				if(xobj.nodeType === 3 || xobj.nodeType === 4) {
					return xobj.nodeValue;
				}
				//Begins a recursive process to build out a JSON structure						
				this.run.apply(opt, [root, xroot]);
				this.setAttributes(root, xroot);
				return root;
			},
			textToXML: function(strXML) {
				var xmlDoc = null;
				try {
					xmlDoc = (util.isIE())?new ActiveXObject("MSXML2.DOMDocument"):new DOMParser();
					xmlDoc.async = false;
				} catch(e) {throw new Error("XML Parser could not be instantiated");}
				var out = null, isParsed = true;
				if(util.isIE()) {
					isParsed = xmlDoc.loadXML(strXML);
					out = (isParsed)?xmlDoc:false;
				} else {
					out = xmlDoc.parseFromString(strXML, "text/xml");
					isParsed = (out.documentElement.tagName !== "parsererror");
				}
				if(!isParsed){throw new Error("Error parsing XML string");}
				return out;
			}
		};
		
		return _public;				 
	})();
	IRoot.prototype = {
		typeOf: "xmlObjectifier",
		attr: function() {
			var out, attr, val;
			if(!!arguments.length) {
				switch(arguments.length) {
					case 1:	attr = util.formatName(arguments[0]);
							val = this["@"+attr]||this[attr];
							if(util.isDef(val) && !util.isArr(val)) { out = val; } break;
					case 2: attr = util.formatName(arguments[0]);
							val = arguments[1];
							if(util.isStr(attr)) { this[(/^@/.test(attr))?attr:"@"+attr] = val; out = this;} break;
				}
			}
			return out;
		},
		find: function(sel) {
			var out = null, tokens, token, tokenOnly;
			if(util.isStr(sel)) {
				var curMatch, subNode;
				var condMatch = /\[(\d+|@\w+(=\w+)?)\]/, conditionStr, parts, partA, partB;
				var rx = /(?=\.)?([A-Za-z\-]+(\[(\d+|@\w+(=\w+)?)\])?)/g;
				var attrMatch = /^@\w+/, attr;
				//If selector is an attribute, then just find attribute and return it
				if(sel.match(attrMatch)) {
					attr = sel.match(attrMatch)[0];
					return this.attr(attr);
				}
				tokens = sel.match(rx);
				if(!!tokens.length) {
					var t = 0, tLen = tokens.length-1;
					do {								
						token = tokens[t];
						tokenOnly = token.match(/[A-Za-z\-]+/)[0];
						curMatch = !!curMatch?(util.isArr(curMatch)?curMatch[0]:curMatch)[tokenOnly]:this[tokenOnly];
						if(!curMatch) { break; }
						conditionStr = !!token.match(condMatch) && token.match(condMatch)[0].replace("[","").replace("]","");
						if(conditionStr && !!conditionStr.length) {
							if(conditionStr.indexOf("=")!==-1) {
								parts = conditionStr.split("=");
								partA = util.trim(parts[0]); partB = util.trim(parts[1]);
								if(partA.indexOf("@")!==-1) {
									attr = partA;
									curMatch = curMatch.getNodesByAttribute(attr, partB);
								} else {
									subNode = util.trim(curMatch[partA]);
									if(subNode) {
										curMatch = subNode.getNodesByValue(partB);
									}
								}
							} else if(util.isNum(conditionStr)){ 
								curMatch = curMatch[parseInt(conditionStr, 10)];
							}
						}								
					} while(t++ < tLen);
					out = curMatch;
				}
			}
			return out;
		},
		addComment: function(comment) {
			if(util.isStr(comment)) {
			if(!util.isDef(this.$comments)) { this.$comments = []; }
				this.$comments.push(comment);
			}
			return this;
		},
		val: function(v) {
			var out = this;
			if(util.isDef(v)) {
				this.Text = v;
			} else { out = this.Text; }
			return out;
		},
		toXML: function() {
			var root = XMLSerializer.newDocument(this.nodeName, this.ns);
			XMLSerializer.objToNode.call(root, root.documentElement, this);
			return root;
		},
		toString: function() {
			var root = this.toXML();
			var out = "";
			if(util.isDef(root.xml)) {
				out = root.xml;
			} else if(util.isDef(XMLSerializer)){
				var serializer = new window.XMLSerializer();
				out = serializer.serializeToString(root);
			}
			return out;
		},
		appendChild: function(nodeClassInst) {
			if(util.isDef(nodeClassInst) && nodeClassInst instanceof INode) {
				nodeClassInst.parent = this;
				if(!util.isDef(this[nodeClassInst.nodeName])) { this[nodeClassInst.nodeName] = XMLObjectifierEngine.makeNodeSet(); }
				this[nodeClassInst.nodeName].push(nodeClassInst);
			}
		}
	};
	INode.prototype = {
		attr: IRoot.prototype.attr,
		val: IRoot.prototype.val,
		find: IRoot.prototype.find,
		addComment: IRoot.prototype.addComment,
		appendChild: IRoot.prototype.appendChild
	};
	//All Public members
	var _publicMapping = {
		xmlToJSON: function(xdoc, opt) {
			return XMLObjectifierEngine.init(xdoc, opt);
		},
		//Converts Text to XML DOM
		textToXML: XMLObjectifierEngine.textToXML,
		//Classes exposed for prototype extensibility
		xmlObjectifier: {
			RootClass: IRoot, //Root node only
			NodeClass: INode,  //Node element
			NodeSetClass: INodeSet //NodeSet class
		}
	};

	if(util.isDef($)) {
		$.extend ( _publicMapping );
	} else {
		window.XMLObjectifier = _publicMapping;
	}
})((typeof(jQuery) !== "undefined") && jQuery || undefined);