/**
 * XhrTree
 * 
 * @version 0.8.8, 2010-06-11
 * @author Gregor Kofler
 *
 * @param {object} container the container element
 * @param {object} entries the initial entries
 * @param {boolean} checkBoxes indicates whether checkboxes should be shown and handled
 * @param {object} requestCommand a set of request commands for the xhr functionality
 *
 * properties of nodes in entries
 * string	key
 * string	text
 * string	link
 * object	subtree
 * obj		parentNode
 * obj		elem (DOM reference to complete li node)
 * obj		stElem (DOM reference to subtree ul node)
 * obj		cbElem (DOM reference to checkbox image node)
 * obj		textElem (DOM reference to text or anchor node)
 * obj		iconElem (DOM reference to optional icon)
 * bool		stVisible
 * bool		lastEntry
 * string	icon
 * bool		disabled
 * int		childnodes
 * int		level
 * int		checked (tri-state)
 * 
 * @return object to manipulate the tree with the following methods
 * 	show()
 *	refresh()
 *  updateNodes(array entries)
 *	setMiscXhrData(obj misc data)
 *	setCheckboxImages (obj images)
 *	setForceRequest(bool flag)
 *  getSelectedKeys(obj keys)
 * 
 * custom xhr response handlers can be added with obj.on_{requestCommand} = function() {}
 * 
 * @todo wrong position of xhrImg upon expanding subtree in Chrome, Safari - scrollOffset not considered
 * @todo replace internal callbacks and functionality by events
 * 
 */

/*global document, vxJS, Error*/

if(!this.vxJS || !this.vxJS.xhr) { throw Error("widget.xhrTree: vxJS core or vxJS.xhr missing."); }

vxJS.widget.xhrTree = function(container, entries, checkBoxes, xhrReq, commands) {

	if(!container) { throw Error("widget.xhrTree: no container defined."); }

	var xhr, forceRequest, miscXhrData = {}, tasks = [], xhrImg = {}, xhrImgSize = {},
		rc = {
			getSubTree: commands && commands.getSubTree ? commands.getSubTree : "xhrTree_getSubTree",
			getInitTree: commands && commands.getInitTree ? commands.getInitTree : "xhrTree_getInitTree",
			refresh: commands && commands.refresh ? commands.refresh : "xhrTree_refresh"
		},
		img = { 
			unChecked: "js/un_checked.gif",
			checked: "js/checked.gif",
			partChecked: "js/part_checked.gif"
		}, 
		icons = {},
		that = {},
		disabledProperties = "";

	xhrReq = vxJS.merge(xhrReq || {}, { echo: 1 });

	xhrImg.subTree = function() {
		var i = "img".setProp("src", "js/assets/xhr_activity_treenode.gif").create();
		i.style.position = "absolute";
		vxJS.dom.setElementPosition(i,  { x: -100, y: -100 } );
		document.body.appendChild(i);
		return i;
	}();
	xhrImg.refresh = function() {
		var i = xhrImg.subTree.cloneNode(true);
		i.src = "js/assets/xhr_activity.gif";
		document.body.appendChild(i);
		return i;
	}();
	
	var findEntry = function(k, e, p) {
		var i, node = null;
		e = e || entries;
		p = p || "key";

		for(i = e.length; i--; ) {
			if(e[i][p] == k) {
				node = e[i];
				break;
			}
			if(e[i].subtree) {
				node = arguments.callee(k, e[i].subtree, p);
				if(node) { break; }
			}
		}
		return node;
	};
	
	var walk = function(p, v, e) {
		e = e || entries;
		e[p] = v;
		if(e.subtree) {
			walk(p, v, e.subtree);
		}
	};

	var setChildCBox = function(n) {
		var i, disabled = !!n.disabled, ticked = !!n.checked, src, st;

		src = ticked ? img.checked : img.unChecked;

		if(!checkBoxes) { return; }

		if(n.subtree) {
			st = n.subtree;
			for(i = st.length; i--; ) {
				if(disabled) {
					st[i].disabled = true;
					st[i].cbElem.className = "__check__"+st[i].key+" disabled";
				}
				else if(!st[i].disabled) {
					st[i].checked = ticked;
					st[i].cbElem.src = src;
				}
				setChildCBox(st[i]);
			}
		}
	};

	var setParentCBox = function(n) {
		var i, p, st, disabled = 0, ticked = 0, semi, wIcons = 0, dIcons = 0;

		if(!n || !(p = n.parentNode)) { return; }

		st = p.subtree;

		for(i = st.length; i--;) {
			if (st[i].disabled) {
				disabled++;
			}
			else {
				ticked += st[i].checked == 1 ? 1 : 0;
				semi = semi || st[i].checked == 2;
				wIcons += st[i].icon === "warn" ? 1 : 0;
				dIcons += st[i].icon === "block" ? 1 : 0;
			}
		}

		p.disabled	= disabled === st.length || disabledProperties.indexOf("checkboxes") !== -1;
		p.checked	= semi ? 2 : (
						!ticked ? 0 : (
							ticked+disabled === st.length ? 1 : 2)); 
		p.icon		= (p.disabled || dIcons === st.length) ? "block" :
						(dIcons || wIcons || disabled ? "warn" : null);

		insertIcon(p);

		if(p.cbElem) {
			p.cbElem.className = "__check__"+p.key;

			if(p.disabled) {
				p.cbElem.className += " disabled";
			}
			else {
				p.cbElem.src = img[p.checked === 0 ? "unChecked" : (p.checked == 1 ? "checked" : "partChecked")];
			}
		}
		setParentCBox(p);
	};

	var displaySubtree = function(n) {
		if(n.stVisible) {
			n.stElem.style.display = "";
		}
		else {
			n.stElem.style.display = "none";
		}
	};

	/**
	 * setEntries allows two different "modes"
	 * e as an array or e as two arrays: tree and checked
	 * in the latter case those arrays get merged
	 * 
	 * e either gets added as a subtree (k !== null) or replaces entries
	 *
	 * @param {String} k
	 * @param {Object} e
	 * @return {Object} tree
	 * 
	 */
	var setEntries = function(k, e) {
		var i, n, p, tree, checked;

		tree = e.tree ? e.tree : e;
		checked = e.tree && e.checked ? e.checked : null;
		
		p = k !== null ? findEntry(k) : null;

		if(tree && tree.length) {
			if (p) {
				p.subtree = tree;
			}
			else {
				entries = tree;
			}
		}

		if(checked && checked.length) {
			for(i = checked.length; i--; ) {
				if((n = findEntry(checked[i].key, tree))) {
					n.checked = +checked[i].state;
				}
			}
		}
		return tree;
	};

	var insertIcon = function(n) {
		if(n.iconElem) {
			n.iconElem.parentNode.removeChild(n.iconElem);
			n.iconElem = null;
		}
		if (n.textElem && n.icon && icons[n.icon]) {
			n.iconElem = "span".setProp("class", "icon").create("img".setProp("src", icons[n.icon]).create());
			n.textElem.parentNode.insertBefore(n.iconElem, n.textElem);
		}
	};

	var setNodeIcon = function(n, state) {
		var e = vxJS.dom.getElementsByClassName("__expand__"+n.key, container)[0];

		if(typeof state === "undefined") {
			state = n.stVisible; 
		}
		e.src = state ? "js/tree_minus.gif" : "js/tree_plus.gif"; 
	};

	var printRow = function(tree, i) {
		var	n = tree[i], k, c, t,
			cbDisabled = disabledProperties.indexOf("checkboxes") !== -1;

		if(typeof n.childnodes == "undefined" || +n.childnodes > 0) {
			k = "img".setProp([
				["src", "js/tree_plus.gif"],
				["class", "childNodesPresent __expand__"+n.key]]).create();
		}
		else {
			k = "img".setProp("src", "js/leaf_node.gif").create();
		}

		if(checkBoxes && !(n.force && n.force.indexOf("nocheckbox") !== 1)) {
			if(typeof n.checked === "undefined" && n.parentNode && n.parentNode.checked !== 2) {
				n.checked = n.parentNode.checked;
			}
			c = "img".setProp([["class", "__check__"+n.key+(n.disabled || cbDisabled ? " disabled" : "")], ["src", img[!n.checked ? "unChecked" : (n.checked == 1 ? "checked" : "partChecked")]]]).create();
		}

		t = n.link ? "a".setProp("href", n.link).create(n.text) : document.createTextNode(n.text);

		n.cbElem = c;
		n.textElem = t;

		n.elem.appendChild("td".setProp("class", i == tree.length-1 ? "withoutTrail" : "withTrail").create(k));

		if (c) {
			n.elem.appendChild("td".setProp("class", "checkbox").create(c));
		}

		n.elem.appendChild("td".setProp("class", "text __link__"+n.key).create());
		n.elem.lastChild.appendChild(t);
		
		insertIcon(n);
	};

	var buildTree = function(key, tree) {
		var i, l, lvl, e, p, q, ul, r;

		tree = tree || entries;

		q = p = key === null ? null : findEntry(key);
		ul	= "ul".setProp("class", "xhrTree").create();
		r	= "tr".create();
		lvl	= p && typeof p.level != "undefined" ? 1+p.level : 0;

		while(q) {
			r.insertBefore("td".setProp("class", q.lastEntry ? "withoutTrail" : "withTrail").create(), r.firstChild);
			q = q.parentNode;
		}

		for(i = 0, l = tree.length; i < l; i++) {
			e = tree[i];

			e.level = lvl; 
			e.parentNode = p;
			e.elem = r.cloneNode(true);
			printRow(tree, i);
			e.elem = "li".create("table".create("tbody".create(e.elem)));
			ul.appendChild(e.elem);
		}

		if (e) {
			e.lastEntry = true;
		}

		if (!p) {
			container.appendChild(ul);
		}
		else {
			if (p.stElem) {
				p.elem.removeChild(p.stElem);
			}
			p.elem.appendChild(ul);
			p.subtree = tree;
			p.stElem = ul;
			p.stVisible = true;
			setNodeIcon(p);
			displaySubtree(p);
		}		

		return tree;
	};

	var toggleSubtree = function(n) {
		if(forceRequest && n.stVisible) {
			if(n.stElem) { n.stElem.parentNode.removeChild(n.stElem); }
			delete n.subtree;
			delete n.stVisible;
			delete n.stElem;
			setNodeIcon(n);
			return;
		}
		if(typeof n.stVisible !== "undefined") {
			n.stVisible = !n.stVisible;
			setNodeIcon(n);
			displaySubtree(n);
			return;
		}
		if(!n.subtree) {
			getSubTree(n);
		}
	};

	var handleClick = function(e) {
		var cls, n, li;
		if(/disabled/.test(this.className)) { return; }

		if((cls = this.className.match(/((?:__expand__)|(?:__check__)|(?:__link__))([a-z0-9_\-]+)/i))) {
			li = vxJS.dom.getParentElement(this, "li");

			switch(cls[1]) {
				case "__expand__":
					toggleSubtree(findEntry(li, null, "elem"));
					return;
				case "__check__":
					n = findEntry(li, null, "elem");
					n.checked = n.checked ? 0 : 1;
					this.src = n.checked ? img.checked : img.unChecked; 
					setChildCBox(n); 
					setParentCBox(n);

					if(typeof that.clickCBox == "function") { that.clickCBox(findEntry(cls[2])); }
					return;
				case "__link__":
					if(typeof that.clickLink == "function") { that.clickLink(findEntry(cls[2])); }
					return;
			}
		}
	};

	var handleXhrResponse = function() {
		var t, i, r = this.response;

		if(!r.echo) { return; }

		if (typeof this["on_" + r.echo.httpRequest] == "function") {
			this["on_" + r.echo.httpRequest](r);
		}
		
		else {
			switch (r.echo.httpRequest) {
				case rc.getInitTree:
					if (!r.response || typeof r.response != "object") {
						throw new Error("widget.xhrTree: no entries defined or downloadable.");
					}

					setEntries(null, r.response);
					buildTree();

					if (typeof this.onInitTree == "function") {
						this.onInitTree();
					}
					break;
					
				case rc.getSubTree:
					if (r.response && typeof r.response == "object") {
						setParentCBox(
							buildTree(
								r.echo.key,
								setEntries(r.echo.key, r.response)
							)[0]);
					}
					break;

				case rc.refresh:
					setEntries(null, r.response);
					refreshTree();
			}
		}

		t = tasks.copy();
		tasks = [];
		while((i = t.shift())) {
			i.f.apply(null, i.args);
		}
	};

	var refreshTree = function(key, tree) {
		var i;

		if(typeof key === "undefined") { key = null; }
		if (!tree) {
			tree = entries;
			vxJS.dom.deleteChildNodes(container);
		}

		buildTree(key, tree);

		for(i = 0; i < tree.length; i++) {
			if(tree[i].subtree) {
				refreshTree(tree[i].key, tree[i].subtree);
			}
		}
	};

	var getActiveNodes = function(tree, p, level) {
		var coll = [];
		level = typeof level == "undefined" ? 0 : level+1;
		if(typeof p == "undefined") { p = null; }

		var i, t, l = tree.length;
		for(i = 0; i < l; i++) {
			t = tree[i];
			if(t.stVisible && t.subtree) {
				coll.push({ key:t.key, parent: p, level: level });
				coll = coll.concat(getActiveNodes(t.subtree, t.key, level));
			}
		}
		return coll;
	};
	
	var getInitTree = function() {
		xhr = vxJS.xhr(vxJS.merge(xhrReq, { command: rc.getInitTree }), miscXhrData);
		vxJS.event.addListener(xhr, "complete", handleXhrResponse);
	};
	
	var getSubTree = function(n, noAni) {
		if(!noAni) {
			vxJS.dom.setElementPosition(
				xhrImg.subTree,
				vxJS.dom.getElementOffset(vxJS.dom.getElementsByClassName("__expand__"+n.key, container)[0]).add({x: 5, y: 5})
			);
		}

		if(xhr) {
			xhr.use(
				vxJS.merge(xhrReq, { command: rc.getSubTree }),
				vxJS.merge({ key: n.key, level: n.level }, miscXhrData),
				noAni ? null : { node: xhrImg.subTree }
			);
		}
		else {
			xhr = vxJS.xhr(
				vxJS.merge(xhrReq, { command: rc.getSubTree }),
				vxJS.merge({ key: n.key, level: n.level }, miscXhrData),
				noAni ? null : { node: xhrImg.subTree }
			);
			vxJS.event.addListener(xhr, "complete", handleXhrResponse);
		}
	};

	vxJS.event.addListener(container, "click", handleClick);

	/**
	 * public methods
	 */
	that.disable = function(s) {
		disabledProperties = s.replace(/[^a-z,]/, "");

		if(s.indexOf("checkboxes") !== -1) {
			walk("disabled", true);
		}
		refreshTree();
	};
	
	that.show = function() {
		if(entries) { buildTree(null, entries); return; }
		 getInitTree();
	};

	that.refresh = function(elem) {
		var p, s;

		if(elem) {
			if(!xhrImgSize.refresh && xhrImg.refresh.complete) {
				xhrImgSize.refresh = vxJS.dom.getElementSize(xhrImg.refresh);
			}
			if(xhrImgSize.refresh) {
				p = vxJS.dom.getElementOffset(elem);
				s = vxJS.dom.getElementSize(elem);
				p.x += s.x/2 - xhrImgSize.refresh.x/2;
				p.y += s.y/2 - xhrImgSize.refresh.y/2;
				vxJS.dom.setElementPosition(xhrImg.refresh, p);
			}
		}
		
		if(xhr) {
			xhr.use(
				vxJS.merge(xhrReq, { command: rc.refresh }),
				vxJS.merge({ nodes: getActiveNodes(entries) }, miscXhrData),
				{ node: xhrImgSize.refresh ? xhrImg.refresh : null }
			);
		}
		else {
			xhr = vxJS.xhr(
				vxJS.merge(xhrReq, { command: rc.refresh }),
				vxJS.merge({ nodes: getActiveNodes(entries) }, miscXhrData),
				{ node: xhrImgSize.refresh ? xhrImg.refresh : null }
			);
			vxJS.event.addListener(xhr, "complete", handleXhrResponse);
		}
	};

	that.updateNodes = function(nodes) {
		var i, n, node, cName, ndx;
		if(!nodes.length) { nodes = [nodes]; }

		if(!entries) {
			tasks.push({f: that.updateNodes, args: arguments});
			return;
		}
		
		for(i = nodes.length; i--;) {
			n = nodes[i];

			if(n.key && (node = findEntry(n.key))) {
				node.text = n.text || node.text;
				node.link = n.link || node.link;

				if(typeof n.icon !== "undefined")		{ node.icon = n.icon; }
				if(typeof n.disabled !== "undefined")	{ node.disabled = n.disabled; }
				if(typeof n.checked !== "undefined")	{ node.checked = n.checked; }
	
				if(node.elem) {
					if(checkBoxes) {
						cName = "__check__"+n.key;
						if(node.disabled || disabledProperties.indexOf("checkboxes") !== -1) {
							cName += " disabled";
						}
						else {
							ndx = node.checked ? "checked" : "unChecked";
						}
						node.cbElem.src = img[ndx];
						node.cbElem.className = cName;

						setChildCBox(node);
						setParentCBox(node);
					}

					node.textElem.parentNode.replaceChild(
						node.link ? "a".setProp("href", node.link).create(node.text) : document.createTextNode(node.text),
						node.textElem);
				}
			}
		}
	};
	
	/**
	 * collects keys with selected and semi-selected checkboxes
	 * @param {Object} e tree entries
	 */
	that.getSelectedKeys = function(e) {
		var i, k = { checked: [], partChecked: [] }, s, n;

		e = e || entries;
		
		if(!e.length) {
			return [];
		}
		for(i = e.length; i--;) {
			n = e[i];
			if(n.checked > 0 ) {
				k[n.checked === 1 ? "checked" : "partChecked"].push(n.key);
			}
			if(n.subtree) {
				s = arguments.callee(n.subtree);
				k.checked.concat(s.checked);
				k.partChecked.concat(s.partChecked);
			}
		}
		return k;
	};

	/**
	 * unfold tree to a specific node
	 * 
	 * @param {Object} keys topdown list of keys 
	 * @param {Object} e optional subtree
	 * @param {Function} cb optional callback invoked when focused entry shown
	 */
	that.focusNode = function(keys, e, cb) {
		var i, k, n;

		if(!keys || !keys.length) { return; }

		e = e ? e.subtree : entries;

		if(!e) {
			tasks.push({f: that.focusNode, args: arguments});
			getInitTree();
			return;
		}

		while(keys.length) {
			k = keys.shift();

			for (i = e.length; i--;) {
				n = e[i];

				if (n.key == k) {
					if (!n.subtree) {
						tasks.push({
							f: that.focusNode,
							args: [keys.copy(), n, cb]
						});
						getSubTree(n, true);
						if(!keys.length && vxJS.fx && typeof cb === "function") {
							cb.call(null, n);
						}
					}
					else {
						if(!n.stVisible) {
							toggleSubtree(n);
						}
						if(!keys.length && vxJS.fx && typeof cb === "function") {
							cb.call(null, n);
						}
						that.focusNode(keys, n, cb);
					}
				}
				else if(n.subtree && n.stVisible) {
					toggleSubtree(n);
				}
			}
			if(!(e = n.subtree)) {
				return;
			}
		}
	};

	that.setCheckboxImages = function(i) {
		if(i.checked)		{ img.checked = i.checked; }
		if(i.unChecked)		{ img.unChecked = i.unChecked; }
		if(i.partChecked)	{ img.partChecked = i.partChecked; }
	};

	that.setIcons = function(i) {
		icons = i;
	};

	that.setForceRequest = function(s) { forceRequest = !!s; }; 

	that.setMiscXhrData = function(o) {
		miscXhrData = o;
	};

	that.setEntries = setEntries;

	that.findEntry = findEntry;
	
	return that;
};