/*
 * Embedded Templates For Ajax (ET4A)
 * Copyright (c) 2006, IBM Corp.
 * Copyright (c) 2006, Stephen Farrell <sfarrell@almaden.ibm.com>
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.  
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the IBM Corp. nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

// declare the IBM.ET4A namespace
var IBM = { ET4A: {} }

// IBM.ET4A.Template is the templating engine itself
// "root" can the an element or its id.
IBM.ET4A.Template = function(root, params) {

	if (typeof root == "string")
		root = document.getElementById(root);
	var template = root.cloneNode(true);
	var bound = false;

	var templateListeners = IBM.ET4A.templateListeners.slice(0); // shallow copy
	if (params && params.templateListener)
		templateListeners.push(params.templateListener);
	for(var i = 0; i < templateListeners.length; i++)
		if (typeof templateListeners[i].init == "function")
			templateListeners[i].init.apply(templateListeners[i],[this, root]);

	var logicHandlers = IBM.ET4A.logicHandlers.slice(0); // shallow copy
	if (params && params.logicHandler)
		logicHandlers.push(params.logicHandler);

	var profiling = {
		totalTime: 0,
		dispatchBindTime: 0,
		dispatchBindCount: 0,
		bindArrayTime: 0
	};

	// Main function.  This binds some data to the template.
	// This function can be called more than once.  If so, it
	// it resets the template before binding the new data.
	this.bind = function(data) {
		var start = new Date().getTime();
		if (bound)
			unbind.apply(this);
		dispatchBind.apply(this, [root, data]);
		bound = true;
		for(var i = 0; i < templateListeners.length; i++)
			if (typeof templateListeners[i].bound == "function")
				templateListeners[i].bound.apply(templateListeners[i],[root, data]);
		profiling.totalTime = new Date().getTime() - start;
	};

	this.rebind = function(tmpl) {
		if(typeof tmpl == "object") {
			template = tmpl;
		} else if (typeof tmpl == "string") {
			template.innerHTML = tmpl;
		}
		this.bind(IBM.ET4A.getNodeData(root));
	};

	// This function is used to fetch data from a URL.  It internally does
	// a bind(), either directly on the resulting data, or, if a callback is
	// provided, and what that callback returns.	
	this.fetch = function(url, callback) {
		var script = document.createElement('script');
		var callbackname = "_ut_callback_" + IBM.ET4A.callbackCounter++;
		var template = this;
		window[callbackname] = function(val) {
			if (callback)
				val = callback(val);
			template.bind(val);
			window[callbackname] = null;
		};
		script.src = url + (url.indexOf("?") == -1 ? "?" : "&") + "callback=" + callbackname;
		script.type = 'text/javascript';
		script.charset = "utf-8"; // right??
		document.body.appendChild(script);
	};


	function dispatchBind(node, data) {
		// accounting
		var start = new Date().getTime();
		profiling.dispatchBindCount++;

		for(var i = 0; i < templateListeners.length; i++) 
			if (typeof templateListeners[i].bind == "function")
				templateListeners[i].bind.apply(templateListeners[i], [node, data]);

		if (node.style && node.style.display == "none")
			node.style.display = "block";

		var classes;
		if (node.nodeType == 1) {
			node._ut_data = data;
			classes = IBM.ET4A.util.parseClasses(node);
			for(var i = 0; i < logicHandlers.length; i++) 
				if(logicHandlers[i].apply(logicHandlers[i],[node, data, classes]))
					return;
		}

		if (data == undefined)
			return;

		if (typeof data == "object") {
			if (node.nodeType != 1)
				return;

			if (data instanceof Array)
				bindArray.apply(this,[node, data]);
			else 
				bindObject.apply(this,[node, data, classes]);
		} else {
			// inlined bindValue()

			for(var i = 0; i < templateListeners.length; i++) 
				if (typeof templateListeners[i].bindValue == "function")
					templateListeners[i].bindValue.apply(templateListeners[i], [node, data]);

			if (node.childNodes.length > 0)
				node = selectValueTarget.apply(this,[childNodesAsArray(node)]);
			if (node.nodeType == 1) {
				//alert(data + " / " + node);
				node.insertBefore(document.createTextNode(data), node.firstChild);
			} else {
				// text node
				//alert(data + " / " + node.nodeValue);
				node.nodeValue = data + node.nodeValue;
			}
		}
		profiling.dispatchBindTime += new Date().getTime() - start;
	}

	function childNodesAsArray(node) {
		var array = [];
		for(var i = 0; i < node.childNodes.length; i++)
			array.push(node.childNodes[i]);
		return array;
	}

	function cloneEachNode(array) {
		var clones = [];
		for(var i = 0; i < array.length; i++)
			clones.push(array[i].cloneNode(true));
		return clones;
	}

	function bindArray(node, array) {

		for(var i = 0; i < templateListeners.length; i++) 
			if (typeof templateListeners[i].bindArray == "function")
				templateListeners[i].bindArray.apply(templateListeners[i], [node, array]);


		if (array.length == 0) {
			while(node.hasChildNodes())
				node.removeChild(node.firstChild);
			return;
		}

		var start = new Date().getTime();

		var nodes = childNodesAsArray(node);

		var clones;
		if (array.length > 1)
			clones = cloneEachNode(nodes);

		for(var i = 0; i < array.length; i++) {
			if (i == 0)
				;
			else if (i < array.length - 1)
				nodes = cloneEachNode(clones);
			else
				nodes = clones;
			if (typeof array[i] == "object") {
				if (array[i] instanceof Array) {
					for(var j = 0; j < nodes.length; j++) {
						if (i > 0)
							node.appendChild(nodes[j]);
						if (nodes[j].nodeType == 1)
							dispatchBind.apply(this, [nodes[j], array[i]]);
					}
				} else {
					var obj = array[i];
					
					// handle delimiter
					if (i == array.length - 1) {
						var l = nodes[nodes.length - 1];
						if (l.nodeType == 3)
							l.nodeValue = "";
					}

					for(var j = 0; j < nodes.length; j++) {
						if (i > 0)
							node.appendChild(nodes[j]);
						if (nodes[j].nodeType == 1)
							dispatchBind.apply(this,[nodes[j], obj]);
					}
				}
			} else { // value

				if (i > 0 || nodes.length == 0) {
					if (nodes.length == 0)
						nodes.push(document.createTextNode(""));
					for(var j = 0; j < nodes.length; j++)
						node.appendChild(nodes[j]);
				}

				// handle delimiter
				if (i == array.length - 1) {
					var l = nodes[nodes.length - 1];
					if (l.nodeType == 3)
						l.nodeValue = "";
				}

				var t = selectValueTarget.apply(this, [nodes]); 
				dispatchBind.apply(this,[t, array[i]]);
			}
		}
		profiling.bindArrayTime += new Date().getTime() - start;
	}

	// Return the node where to stick a value (string/int/etc)
	function selectValueTarget(nodes) {
		for(var i = 0; i < nodes.length; i++) {
			if (nodes[i].nodeType == 1 && !IBM.ET4A.emptyTags[nodes[i].tagName.toLowerCase()]) {
				if (nodes[i].childNodes.length == 0) {
					var n = document.createTextNode("");
					nodes[i].appendChild(n);
					return n;
				} else {
					return selectValueTarget.apply(this,[childNodesAsArray(nodes[i])]);
				}
			}
		}		
		return nodes[0];
	}


	var attrRegexp = /data.(\S+)/g;

	function bindObject(node, obj, classes) {
		// handle exception cases for tags using
		// data(var) syntax
		for(var i = 0; i < node.attributes.length; i++) {
			var attr = node.attributes[i];
			var match = attrRegexp.exec(attr.value);
			if (match) {
				attr.value = attr.value.replace(attrRegexp,obj[match[1]]);
				//delete obj[match[1]];
			}
		}
		
		for(var i = 0; i < templateListeners.length; i++) 
			if (typeof templateListeners[i].bindObject == "function")
				templateListeners[i].bindObject.apply(templateListeners[i], [node, obj, classes]);
		
		var tagname = node.tagName.toLowerCase(); // XHTML
		var hname = node.namespaceURI == undefined ? tagname : node.namespaceURI + " " + tagname;
		var handler = IBM.ET4A.tagHandlers[hname];
		if (handler == undefined)
			handler = IBM.ET4A.defaultTagHandler;
		var bound = handler.apply(this, [dispatchBind, node, obj, classes]);

		if (!bound) {
			for(var i = 0; i < node.childNodes.length; i++) 
				dispatchBind.apply(this, [node.childNodes[i], obj]);
		}
	}


	function unbind() {
		while(root.hasChildNodes())
			root.removeChild(root.firstChild)
		for(var i = 0; i < template.childNodes.length; i++) {
			root.appendChild(template.childNodes[i].cloneNode(true));
		}
	}
}

IBM.ET4A.tagHandlers = {
	a: function(dispatch, node, obj, classes) {
		var bound = false;
		var len = classes.length;
		// HREF BODY REL TITLE
		if (len > 0 && obj[classes[0]] != undefined) {
			node.href = obj[classes[0]];
			//delete obj[classes[0]];
		}
		if (len > 1 && obj[classes[1]] != undefined) {
			dispatch(node, obj[classes[1]]);
			//delete obj[classes[1]];
			bound = true;
		}
		if (len > 2 && obj[classes[2]] != undefined) {
			node.rel = obj[classes[2]];
			//delete obj[classes[2]];
		}
		if (len > 3 && obj[classes[3]] != undefined) {
			node.title = obj[classes[3]];
			//delete obj[classes[3]];
		}
		return bound;
	},

	img:  function(dispatch, node, obj, classes) {
		var len = classes.length;
		// SRC ALT TITLE
		if (len > 0 && obj[classes[0]] != undefined) {
			node.src = obj[classes[0]];
			//delete obj[classes[0]];
		}
		if (len > 1 && obj[classes[1]] != undefined) {
			node.alt = obj[classes[1]];
			//delete obj[classes[1]];
		}
		if (len > 2 && obj[classes[2]] != undefined) {
			node.title = obj[classes[2]];
			//delete obj[classes[2]];
		}

		// empty images show up as broken, so just
		// remove them.
		if (node.src == undefined || node.src == "")
			node.parentNode.removeChild(node);
	}
};


// these are the empty tags in HTML 4.0
IBM.ET4A.emptyTags = {area:1, base:1, basefont:1, br:1, col:1, frame:1, hr:1, img:1, input:1, isindex:1, link:1, meta:1, param:1};

IBM.ET4A.setTagHandler = function(fn, tagname, tagnamespace) {
	if (tagnamespace != undefined)
		IBM.ET4A.tagHandlers[tagnamespace + " " + tagname] = fn;
	else
		IBM.ET4A.tagHandlers[tagname] = fn;
};


IBM.ET4A.defaultTagHandler = function(dispatch, node, obj, classes) {
	var bound = false;
	var len = classes.length;
	// BODY TITLE
	if (len > 0 && obj[classes[0]] != undefined)  {
		dispatch(node, obj[classes[0]]);
		//delete obj[classes[0]];
		bound = true;
	}
	if (len > 1 && obj[classes[1]] != undefined) {
		node.title = obj[classes[1]];
		//delete obj[classes[1]];
	}
	return bound;
};



// A template listener is a property list with any of these functions:
// 	init(template, root) called when template constructed
// 	bindObject(node, object, classes)
// 	bindArray(node, array)
// 	bindValue(node, value)
//	bind(node,data) called regardless of what data type is
// 	bound(root,data) called 1 time after template is bound
// Their return value is ignored
IBM.ET4A.templateListeners = [];
IBM.ET4A.addTemplateListener = function(handler) {
	IBM.ET4A.templateListeners.push(handler);
};


// A logic handler is a function that is called on a node and 
// data object.  A return value of true means stop processing,
// and no further handlers are called after the first true result.
IBM.ET4A.logicHandlers = [	
	function(node, data, classes) {
		// One bad thing about eval is the evaled code runs
		// *inside* the closure context, with complete access
		// to "private" variables and functions. 
		if (node.tagName == "SPAN" && node.className == "script") {
			// set up some vars for the eval call
			var scriptnode = node;
			node = node.parentNode;
			data = IBM.ET4A.getNodeData(node);
			eval(scriptnode.innerHTML);
			// remove the "script" from the dom...
			// not really necessary, i guess.
			node.removeChild(scriptnode);
			return true;
		}
	},

	function(node, data, classes) {
		// I'm trying to figure out why sometimes I seem to need an try{}
		// and sometimes not.
		//alert(classes);
		for (var i = 0; i < classes.length; i++) {
			if (classes[i].match(/^when\(.*\)$/)) {
				var test = classes[i].substring(5, classes[i].length - 1);
				//var res = true;
				//try { res = eval(test) } catch(e) { };
				//alert(test + " " + eval(test));
				var res = eval(test);
				if(!res) {
					node.parentNode.removeChild(node);
					return true;
				} else {
					return false;
				}
			} else if (classes[i].match(/^case\(.*\)$/)) {
				var switchnode = node.parentNode;
				while(switchnode && (!switchnode.className || !switchnode.className.match(/\bswitch\b/)))
					switchnode = switchnode.parentNode;

				if (!switchnode)
					return false;

				if (switchnode._ut_switch) {
					node.parentNode.removeChild(node);
					return true;
				} else {
					var test = classes[i].substring(5, classes[i].length - 1);
					//var res = true;
					//alert(test + " / " + eval(test));
					//try { res = eval(test) } catch(e) { };
					var res = eval(test);
					if(!res) {
						node.parentNode.removeChild(node);
						return true;
					} else {
						switchnode._ut_switch = true;
						return false;
					}
				}
			}
		}
	}
];

IBM.ET4A.addLogicHandler = function(handler) {
	IBM.ET4A.logicHandlers.push(handler);
};

IBM.ET4A.callbackCounter = 0;

IBM.ET4A.getNodeData = function(node) {
	return node._ut_data;
};
	

// Related utilities
IBM.ET4A.util = {
	// Generic event watching wrapper for IE/Mozilla compatibility
	watchEvent: function(element, name, observer, useCapture) {
		if (element.addEventListener) element.addEventListener(name, observer, useCapture);

       		else if (element.attachEvent) element.attachEvent('on' + name, observer);
	},

	hyphenReg: /-./g,

	trimReg: /(^\s+|\s+$)/g,

	parseClasses: function(node) {
		var classes = [];
		if (node == undefined)
			return classes;
		var str = node.getAttribute("class");
		if (str == undefined)
			str = node.className;
		if (str == undefined)
			return classes;
		if (typeof str == "object")
			return classes;

		if(str.indexOf(' ') == -1) {
			classes.push(str);
		} else if (str.indexOf('(') == -1) {
			classes = str.split(/\s+/);
		} else {
			// my tribute to the hand-parser....
			var quote, esc;
			var paren = 0;
			var cur = "";
			for (var i = 0; i < str.length; i++) {
				var c = str.charAt(i);
				if (quote) {
					if (esc) {
						esc = false;
					} else {
						if (c == quote)
							quote = null;
						else if (c == '\\')
							esc = true;
					}
				} else {
					if (c == '(')
						paren++;
					else if (c == ')')
						paren--;
				}
				cur += c;
				if (paren == 0 && c == ' ') {
					classes.push(cur.replace(IBM.ET4A.util.trimReg, ''));
					cur = "";	
				}
			}
			classes.push(cur);
		}

		for(var i = 0; i < classes.length; i++) {
			var cls = classes[i];
			if (cls.indexOf('(') == -1) {
				classes[i] = cls.replace(IBM.ET4A.util.hyphenReg,
					function(w) {
						return w.substring(1).toUpperCase()
					});
			}
		}
		return classes;
	}
};

// give push() method to arrays if necessary (e.g., IE)
if (!Array.prototype.push) {
	Array.prototype.push = function (element) {
  		this[this.length] = element;
	}
}
