/**
 * XhrTree
 * 
 * @version 0.7.0, 2008-09-02
 * @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 row node)
 * obj		stElem (DOM reference to subtree list 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)
 *	setUri(string uri)
 *	setMiscXhrData(obj misc data)
 *	handleXhrResponse(obj response)
 *	setAsRefreshButton(obj elem)
 *	setCheckboxImages (obj images)
 *	setForceRequest(bool flag)
 *  getSelectedKeys(obj keys)
 * 
 * custom xhr response handlers can be added with obj.on_{requestCommand} = function() {}
 */

if(!vxJS || !vxJS.xhr) { throw new Error("widget.xhrTree: vxJS core or vxJS.xhr missing."); }

vxJS.widget.xhrTree = function(container, entries, checkBoxes, requestCommand) {

	if(!container) { throw new Error("widget.xhrTree: no container defined."); }

	var hasCheckBoxes = !!checkBoxes,
		xhr, forceRequest, refreshButton, miscXhrData, tasks = [],
		rc = {
			getSubTree: requestCommand && requestCommand.getSubTree ? requestCommand.getSubTree : "xhrTree_getSubTree",
			getInitTree: requestCommand && requestCommand.getInitTree ? requestCommand.getInitTree : "xhrTree_getInitTree",
			refresh: requestCommand && requestCommand.refresh ? requestCommand.refresh : "xhrTree_refresh"
		},
		img = { 
			unChecked: "/js/un_checked.gif",
			checked: "/js/checked.gif",
			partChecked: "/js/part_checked.gif"
		}, flashColor = "#aca", 
		icons = {},
		that = {},
		disabledProperties = "";

	var addMiscXhrData = function() {
		if(!miscXhrData) { return; }
		for(var i in miscXhrData) {
			if(miscXhrData.hasOwnProperty(i)) { that.requestParameters[i] = miscXhrData[i]; }
		}
	};

	var findEntry = function(k, e) {
		var i, node = null;
		e = e || entries;
		for(i = e.length; i--; ) {
			if(e[i].key == k) {
				node = e[i];
				break;
			}
			if(e[i].subtree) {
				node = findEntry(k, e[i].subtree);
				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(!hasCheckBoxes) { 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) {
			vxJS.dom.showElement(n.stElem);
		}
		else {
			vxJS.dom.hideElement(n.stElem);
		}
	};

	/**
	 * 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, s, 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(hasCheckBoxes && !(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;
		if(/disabled/.test(this.className)) { return; }

		
		if((cls = this.className.match(/((?:__expand__)|(?:__check__)|(?:__link__))([a-z0-9_\-]+)/i))) {
			switch(cls[1]) {
				case "__expand__":
					toggleSubtree(findEntry(cls[2]));
					return;
				case "__check__":
					n = findEntry(cls[2]);
					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 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() {
		addMiscXhrData();
		that.requestCommand = rc.getInitTree;
		xhr.handleRequest();
	};
	
	var getSubTree = function(n) {
		that.requestParameters = { key: n.key, level: n.level };
		addMiscXhrData();
		that.requestCommand = rc.getSubTree;
		xhr.setAnimation({
			img: "/js/xhr_activity3.gif",
			position: vxJS.dom.getElementOffset(vxJS.dom.getElementsByClassName("__expand__"+n.key, container)[0]).add({x: 5, y: 5})
		});
		xhr.handleRequest();
	};

	vxJS.event.addListener(container, "click", handleClick);

	xhr = vxJS.xhr(that);
	xhr.setEcho(true);

	that.requestParameters = {};

	/**
	 * 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 size;

		this.requestParameters = { nodes: getActiveNodes(entries) };
		addMiscXhrData();
		this.requestCommand = rc.refresh;

		elem = elem || refreshButton;

		if(elem) {
			size = vxJS.dom.getElementSize(elem);
			xhr.setAnimation({
				img: "/js/xhr_activity4.gif",
				position: vxJS.dom.getElementOffset(elem).add({x: size.x/2, y: size.y/2}).sub(new Coord(39, -8))
			});
		}
		xhr.handleRequest();
	};

	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 || cbDisabled) {
							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
	 */
	that.focusNode = function(keys, e) {
		var i, j, k, n, e = e ? e.subtree : entries, loading;

		if(!keys || !keys.length) { return; }

		if(!e) {
			tasks.push({f: that.focusNode, args: arguments});
			getInitTree();
			return;
		}

		while(keys.length) {
			k = keys.shift();

			for (j = e.length; j--;) {
				n = e[j];

				if (n.key == k) {
					if (!n.subtree) {
						tasks.push({
							f: that.focusNode,
							args: [keys.copy(), n]
						});
						getSubTree(n);
						if(!keys.length && vxJS.fx) {
							vxJS.fx.add(n.textElem.parentNode.parentNode, "flash", {originalColor: "#fff", flashColor: flashColor });
						}
					}
					else {
						if(!n.stVisible) {
							toggleSubtree(n);
						}
						if(!keys.length && vxJS.fx) {
							vxJS.fx.add(n.textElem.parentNode.parentNode, "flash", {originalColor: "#fff", flashColor: flashColor });
						}
						that.focusNode(keys, n);
					}
				}
				else if(n.subtree && n.stVisible) {
					toggleSubtree(n);
				}
			}
			if(!(e = n.subtree)) {
				return;
			}
		}
	};

	that.handleXhrResponse = function(resp) {
		var t, i;

		if(!resp.echo) { return; }

		if (typeof this["on_" + resp.echo.httpRequest] == "function") {
			this["on_" + resp.echo.httpRequest](resp);
		}
		
		else {
			switch (resp.echo.httpRequest) {
				case rc.getInitTree:
					if (!resp.response || typeof resp.response != "object") {
						throw new Error("widget.xhrTree: no entries defined or downloadable.");
					}

					setEntries(null, resp.response);
					buildTree();

					if (typeof this.onInitTree == "function") {
						this.onInitTree();
					}
					break;
					
				case rc.getSubTree:
					if (resp.response && typeof resp.response == "object") {
						setParentCBox(
							buildTree(
								resp.echo.key,
								setEntries(resp.echo.key, resp.response)
							)[0]);
					}
					break;

				case rc.refresh:
					setEntries(null, resp.response);
					refreshTree();
			}
		}

		t = tasks.copy();
		tasks = [];
		while((i = t.shift())) {
			i.f.apply(null, i.args);
		}
	};

	that.setXhrUri = function(uri) { xhr.setUri(uri); };

	that.setAsRefreshButton = function(elem) {
		vxJS.event.addListener(elem, "click", function() { that.refresh(); });
		refreshButton = elem;
	};

	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) {
		if(typeof o == "object") { miscXhrData = o; }
	};

	that.setEntries = setEntries;

	that.findEntry = findEntry;
	
	return that;
};
