/**
 * From 20 September 2006 onwards, this class should only be used for browsers that don't support
 * either the ActiveXObject Microsoft.XMLDOM or the W3C compatible Element.
 * @param tagName {String} Required
 * @param parentNode {XMLElement} Optional
 */
function XMLElement(tagName,parentNode) {

	// TODO : delete all external references to this.attributes
	// TODO : support this.firstChild property (rewrite this.refreshChildNodeRefs)

	// W3C DOM Level 2 compatible functions...

	this.appendChild = function(childNode) {
		this.setChild(this.childNodes.length,childNode)}

	this.getAttribute = function(name) { // returns (string) attribute value
		return this.attributes[name]}

	this.getElementsByTagName = function(tagName) {
		var cn = this.childNodes, fcn = [];
		for (var i=0; i<cn.length; i++) {
			if (cn[i].childNodes && cn[i].childNodes.length>0) {
				var matchingElems = cn[i].getElementsByTagName(tagName);
				for (var j=0; j<matchingElems.length; j++) {
					fcn.push(matchingElems[j])}} // concat doesn't work when base array is empty
			if (cn[i].tagName==tagName) fcn.push(cn[i]);}
		return fcn}

	this.removeAttribute = function(name) { // removes an attribute
		delete this.attributes[name]};

	this.removeChild = function(childNode) {
		var pos, cn = this.childNodes;
		for (var i=0; i<cn.length; i++) {
			if (cn[i].nodeRef==childNode.nodeRef) pos=i;}
		var rcn = cn[pos];
		rcn.parentNode = null;
		rcn.nodeRef = '0';
		for (var i=pos; i<cn.length-1; i++) {
			cn[i] = cn[i+1]}
		cn.length--;
		this.refreshChildNodeRefs();
		return rcn}

	this.setAttribute = function(name,value) { // adds or modifies attribute
		this.attributes[name] = (value!=null) ? SpecialXMLChars.unescape(value.toString()) : '';}

	// Proprietary functions...

	this.setChild = function(childIndex,childNode) {
	    childNode.parentNode = this;
	    this.childNodes[childIndex] = childNode;
	    this.refreshChildNodeRefs()}
	
	this.insertChild = function(childNode,index) {
		childNode.parentNode = this;
		this.childNodes.splice(index,0,childNode);
		this.refreshChildNodeRefs()}

	this.getChildData = function() { // returns data of all XMLText child nodes
	    var data = '', cn = this.childNodes;
	    for (var i=0; i<cn.length; i++) {
        	if (cn[i] instanceof XMLText) {
	            if (data.length>0) data += ' ';
	            data += cn[i].data}}
	    return data}

	this.getEscapedChildData = function() {
		return SpecialXMLChars.escape(this.getChildData())}

	this.refreshChildNodeRefs = function() {
	    var cn = this.childNodes;
	    for (var i=0; i<cn.length; i++) {
	    	cn[i].nodeRef = this.nodeRef + '.' + i;
	        if (cn[i].childNodes) {
	        	cn[i].refreshChildNodeRefs();
	        }
	    }
	}

	this.getNodeWithRef = function(nodeRef) { // returns sub node with given nodeRef
	    if (this.nodeRef==nodeRef) return this;
	    var cn = this.childNodes;
	    if (cn) {
	        for (var i=0; i<cn.length; i++) {
	            var n = cn[i].getNodeWithRef(nodeRef);
	            if (n!=null) return n}}}

	/**
	 * Same as appendChild
	 * @deprecated Use appendChild instead
	 */
	this.addChild = this.appendChild;

	/**
	 * Different to getElementsByTagName because only the first level child nodes are matched.
	 * @return Child nodes with given tag name.
	 */
	this.getChildNodesByTagName = function(tagName) {
		var cn = this.childNodes, fcn = [];
		for (var i=0; i<cn.length; i++) {
			if (cn[i].tagName==tagName) fcn.push(cn[i])}
		return fcn}

	/**
	 * @param atts {Object} Map of attribute names to attribute values
	 */
	this.addAttributes = function(atts) {
		for (var i in atts) this.setAttribute(i,atts[i]);}

	/**
	 * @return First child node with given tag name
	 */
	this.getChild = function(tagName) {
		return this.getChildNodesByTagName(tagName)[0]}

	/**
	 * @param {XMLElement} xe
	 */
	this.equals = function(xe) {
		return (xe && xe.toXML().equalsXML(this.toXML()))},

	/**
	 * @param {Number} level Sets the tabulation for the node if undefined or greater than -1.
	 */
	this.toXML = function(level) {
		if (!level) 
			level = 0;
		
		var tab = '';
		if (level>=0) {
			for (i=0; i<level; i++) 
				tab += '    ';
		}
		
		var xml = tab + '<' + this.tagName;
		for (var i in this.attributes) {
			xml += ' ' + i + '="' + SpecialXMLChars.escape(this.attributes[i]) + '"';
		}
		
		if (this.childNodes.length==0) {
			xml += '/>';
			if (level>=0) 
				xml += '\n';
		} else { // element has children
	        xml += '>';
	        if (level>=0)
	        	xml += '\n';
	        
	        var childLevel = (level>=0) ? level+1 : level;
	        for (var i=0; i<this.childNodes.length; i++) 
	        	xml += this.childNodes[i].toXML(childLevel);
	        xml += tab + '</' + this.tagName + '>';
	        if (level>=0) 
	        	xml += '\n';
	    }
		return xml;
	}

	this.toString = this.toXML;

	// Initialiser
	this.tagName = tagName;
	this.attributes = {}; // attributes mapped by name
	this.childNodes = [];
	if (parentNode) parentNode.appendChild(this); // parentNode & nodeRef set by parent
	else this.nodeRef = '0'; // Proprietary attribute
}


XMLTools = {

	/**
	 * @param {String} xml (Required) The XML to be parsed
	 * @return {Element} The root element of the document. XMLElement returned if native
	 * Element is not supported.
	 */
	parseDocumentElement : function(xml) {
		var xd, xe;
		var w = window;
		// IE
		if (w.ActiveXObject) {
			xd = new ActiveXObject("Microsoft.XMLDOM");
			xd.async = false;
			var isLoaded = xd.loadXML(xml);
			xe = (isLoaded) ? xd.documentElement : this.parseDocumentXE(xml);}
		// Mozilla, Firefox, Opera, etc.
		else if (w.DOMParser && w.XMLSerializer) {
			xd = (new DOMParser()).parseFromString(xml,'text/xml');
			xe = xd.documentElement}
		// other browsers
		else xe = this.parseDocumentXE(xml);
		return xe},

	/**
	 * @param {String} xml
	 * @return {XMLElement}
	 */
	parseDocumentXE : function(xml) {
		var xe;
		xml = xml.replace(/\s+/g,' ');
		var tagName, att, data, c;
		var inTag = false;
		var ignore = false;
		for (var i=0; i<xml.length; i++) {
			c = xml.charAt(i);
			if (ignore) { // tag to be ignored : comment, doctype, xml declaration
				if (c=='>') ignore = false;}
			else if (tagName!=null) { // getting tag name
				if (c==' ' || c=='/' || c=='>') {
					xe = new XMLElement(tagName,xe);
					tagName = null;
					if (c=='/' && xe.parentNode) xe = xe.parentNode;
					else if (c=='>') inTag = false;
					else if (c==' ') att = [];}
				else tagName += c;}
			else if (att!=null) { // getting attribute
				if (att[1]!=null) { // ...value
					if (c=='"') {
						xe.setAttribute(att[0],att[1]);
						att = null;}
					else att[1] += c;}
				else if (att[0]!=null) { // ...name
					if (c=='"') att[1] = '';
					else if (c!='=') att[0] += c;}
				else if (c!='/' && c!='>') { // ...name
					att = [c];}
				else { // ...end
					att = null;
					if (c=='/' && xe.parentNode) xe = xe.parentNode;
					else if (c=='>') inTag = false;}}
			else if (inTag) { // transition
				if (c=='/' && xe.parentNode) xe = xe.parentNode;
				else if (c=='>') inTag = false;
				else if (c==' ') att = [];}
			else if (c=='<') { // new tag
				if (data && data!='') new XMLText(data.trim(),xe);
				data = null;
				var nc = xml.charAt(i+1);
				if (nc=='!' || nc=='?') ignore = true;
				else {
					inTag = true;
					if (nc!='/') tagName = '';}}
			else if (data!=null) data += c; // getting text node data
			else data = c;} // new text node
		return xe},

	serializeToXML : function(elem) {
		if (!elem) return;
		if (elem.xml) // MSIE  (in fact, IE < 8)
			return elem.xml;
		else if (!$j.browser.msie && elem instanceof Element && window.XMLSerializer) // Mozilla, Firefox, Opera, etc.
			return (new XMLSerializer()).serializeToString(elem);
		else if (elem.toXML) // other browsers
			return elem.toXML(-1);
	},

	convertToXE : function(elem) {
		return new ParsedXMLElement(this.serializeToXML(elem))},

	/**
	 * @param {NodeMap} nodeMap
	 * @return {Array} Array of nodes
	 */
	nodeMapToArray : function(nodeMap) {
		if (nodeMap instanceof Array) return nodeMap;
		else {
			var array = [];
			for (var i=0; i<nodeMap.length; i++) {
				array.push(nodeMap.item(i))}
			return array}},

	prettyPrint : function(xml) {
		// TODO : parse xml and add tabs
		return xml.replace(/\s+/g,' ').replace(/</g,'\n<')},

	/**
	 * Gets the child elements of an element. Differs from the childNodes attribute because nodes
	 * include text (and spacing chars, line returns, etc) as well as elements.
	 * @param {String} tagName (Optional) Child nodes can be filtered by tag name, if given.
	 * @return {[Element]}
	 */
	getChildElements : function(elem,tagName) {
		if (!elem) return;
		var cn = elem.childNodes, ce = [];
		for (var i=0; i<cn.length; i++) {
        	if (cn[i].tagName && cn[i].nodeName!="#comment" && (!tagName || tagName==cn[i].tagName)) {
        		ce.push(cn[i])}}
	    return ce},

	/**
	 * @param {Element} elem Or XMLElement if native Element not supported
	 * @return {String} Data of all XMLText or Text child nodes or an empty string
	 */
	getChildData : function(elem) { //
	    var data = '', cn = elem.childNodes;
	    for (var i=0; i<cn.length; i++) {
        	if (cn[i] instanceof XMLText || cn[i].data!=null) {
	            if (data.length>0) data += ' ';
	            data += cn[i].data}}
	    return data}
}


/**
 * @constructor Parses an XML string to build a XMLElement
 * @extends XMLElement
 * @param {String} xml (Required) XML string to be parsed
 * @param {XMLNode} parentNode (Optional) The parent node of this parsed XMLElement
 * @deprecated 27 Sep 2006
 */
function ParsedXMLElement(xml,parentNode) {
	var str = xml;

    // remove comments
    while (str.match(/<!--/)) {
    	if (!str.match(/-->/)) throw new Error("A comment tag in this XML file is unclosed...\n\n\""+xml+"\"");
    	var cStart = str.search(/<!--/); // comment start index
    	var cEnd = str.search(/-->/) + 3; // comment end index
    	str = str.substring(0,cStart) + str.substr(cEnd)}

    // remove xml & doctype tags
    str = str.replace(/<\?xml[^>]+>/,'').replace(/<!DOCTYPE[^>]+>/,'');

    var tMatch = str.match(/<[^>]+>/);
    var tag = (tMatch) ? tMatch[0] : null;
    if (!tag) throw new Error("An element in this XML file is badly formed...\n\n\""+xml+"\"");

    var wMatch = tag.match(/\w+/); // words of first tag
    var tagName = (wMatch) ? wMatch[0] : null;
    if (!tagName) throw new Error("An element in this XML file is missing the tag name...\n\n\""+xml+"\"");

    // set mandatory attribs of XMLElement
    this.tagName = tagName;
    this.attributes = {}; // attributes mapped by name
    this.childNodes = [];
    if (parentNode) parentNode.appendChild(this); // parentNode & nodeRef set by parent
    else {
        this.parentNode = null;
        this.nodeRef = '0'}

    // set attributes member
    var attribs = tag.match(/\w+="[^"]*"/g);
    if (attribs) {
        for (var i=0; i<attribs.length; i++) {
            var xa = attribs[i];
            var name = xa.match(/\w+/g)[0];
            var value = xa.match(/[^"]+/g)[1];
            this.setAttribute(name,value)}}

    // strip tag & leave inner content if applicable
    var sp = str.indexOf('>') + 1; // start position
    var ep = (str.charAt(sp-2)=='/') ? str.length : str.lastIndexOf('<'); // end position
    str = str.substring(sp,ep).trim(); // trim resulting str
    while (str!='') {
        if (/^</.test(str)) { // element
            // find corresponding end tag
            var i = 0, level = 0, isPCT = false, isFound = false; // isPCT = is pending closing tag
            while (i<str.length && !isFound) {
                switch (str.charAt(i)) {
                    case '<' :
                        if (i+1<str.length && str.charAt(i+1)=='/') {isPCT = true; i+=2} // '</'
                        else {level++; i++} // '<'
                        break;
                    case '>' : // '>'
                        if (isPCT) {isPCT = false; level--}
                        i++;
                        break;
                    case '/' :
                        if (i+1<str.length && str.charAt(i+1)=='>') {level--; i+=2; break} // '/>'
                    default : i++}
                isFound = (level==0);
            }
            var eStr = str.substr(0,i); // element string
            new ParsedXMLElement(eStr,this);
            str = (i<str.length) ? str.substr(i) : '';
        }
        else { // text
            // find last char before another element
            var data = str.match(/[^<]+/g)[0];
            new XMLText(data.trim(),this);
            str = str.substr(data.length)}
        str = str.trim()}
}

ParsedXMLElement.prototype = new XMLElement();


/**
 * @param data {String} Required
 * @param parentNode {XMLNode} Optional
 */
function XMLText(data,parentNode) {

	// initialiser
	this.childNodes; // is a leaf node therefore is undefined
	this.data = (data) ? SpecialXMLChars.unescape(data) : ''; // (string)
	if (parentNode) parentNode.appendChild(this); // parentNode & nodeRef set by parent
	else {
		this.parentNode;
		this.nodeRef = '0'}

	this.getNodeWithRef = function(nodeRef) { // returns sub node with given nodeRef
	    if (this.nodeRef==nodeRef) return this;}

	this.getEscapedData = function() {
		return SpecialXMLChars.escape(this.data)}

	/**
	 * @param {Number} level Sets the tabulation for the node if undefined or greater than -1.
	 */
	this.toXML = function(level) {
	    if (!level) level = 0;
	    var xml = '';
	    if (level>=0) {
	    	for (i=0; i<level; i++) xml += '    ';}
	    xml += this.getEscapedData();
	    if (level>=0) xml += '\n';
	    return xml}

	this.toString = this.toXML;
}


/**
 * @param publicID {String} Required
 * @param systemID {String} Required
 * @param charset {String} Required
 * @param xeRoot {XMLElement} Required. The root element of the document.
 */
function XMLDocument(publicID,systemID,charset,xeRoot) {

	this.publicID = publicID;
	this.systemID = systemID;
	this.charset = charset;
	this.xeRoot = xeRoot;

	/**
	 * @param {Number} level Sets the tabulation for the child nodes if undefined or greater than -1.
	 */
	this.toXML = function(level) {
	    return '<?xml version="1.0" encoding="'+this.charset+'"?>\r\n'
	    + '<!DOCTYPE '+this.xeRoot.tagName+' PUBLIC "'+this.publicID+'" "'+this.systemID+'">\r\n'
	    + this.xeRoot.toXML(level)}

	this.toString = this.toXML;
}


/**
 * Maps keys to values. Cannot contain duplicate keys. Each key can map to at most one value.
 * @param arguments (optional) key0, value0, key1, value1, ..., keyx, valuex
 */
function Map() {

	this.items = [];

	this.put = function(key,value) {
		if (!key || !value) throw new Error('The key and the value of a map entry must be defined');
		for (var i=0; i<this.size(); i++) {
	    	if (this.getKey(i)==key) {
	    		 this.items[i].value = value;
	    		 return}}
		this.items.push({key:key, value:value})}

	this.get = function(key) {
		for (var i=0; i<this.size(); i++) {
	    	if (this.getKey(i)==key) return this.getValue(i)}}

	this.size = function() {
		return this.items.length}

	this.getKey = function(index) {
		return this.items[index].key}

	this.getValue = function(index) {
		return this.items[index].value}

	this.sort = function() {
		var itemSorter = function(a,b) {
			if (a.key < b.key) return -1;
			if (a.key > b.key) return 1;
			return 0}
		this.items.items(itemSorter)}

	// initialiser
	var a = arguments;
	if (a) {for (var i=0; i<a.length; i+=2) this.put(a[i], a[i+1])}
}


//*****************************************************
// String prototypes
//*****************************************************

String.prototype.trim = function() {
	// returns string with leading & tailing spaces removed
	return this.replace(/^\s+/g,'').replace(/\s+$/,'');
}

String.prototype.normaliseXML = function() {
	// returns a single line string with a minimum of spaces and no leading & tailing spaces
	return this.trim().replace(/[\f\r\n\t\v]/g,'').replace(/\s{2,}/g,' ').replace(/> </g,'><');
}

String.prototype.equalsXML = function(str) {
	// tests if this XML string is equal to given XML string
	return (this.normaliseXML()==str.normaliseXML());
}


/**
 * Static class for escaping and unescaping special XML characters
 */
SpecialXMLChars = {

	// TODO : move these methods to XMLTools

	map : new Map(
		'&','&amp;',
		'>','&gt;',
		'<','&lt;',
		'"','&quot;',
		"'",'&apos;'),

	escape : function(str) {
		var m = this.map;
		for (var i=0; i<m.size(); i++) {
			str = str.replace(new RegExp(m.getKey(i),'g'), m.getValue(i))}
		return str},

	unescape : function(str) {
		var m = this.map;
	    for (var i=m.size()-1; i>=0; i--) { // reverse order to replace '&' at the end
			str = str.replace(new RegExp(m.getValue(i),'g'), m.getKey(i))}
		return str}
}

