/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. 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.
 * 3. Neither the name of "Scrivo" 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 HOLDER 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.
 *
 * $Id: TreeView.js 616 2013-04-22 23:48:38Z geert $
 */

"use strict";

SUI.TreeView = SUI.defineClass(
	/** @lends SUI.TreeView.prototype */{

	/** @ignore */ baseClass: SUI.Box,

	/**
	 * @class
	 * SUI.TreeView is an asynchronious tree control. In a tree control a set
	 * of items is displayed in a hierarchical manner, allowing the user to
	 * expand sections to dig deeper into the hierarchy. This control is always
	 * loaded with server side data, so it will not work on your desktop alone.
	 *
	 * @augments SUI.Box
	 *
	 * @description
	 * Create a treeview contol. If you want to do your own xhr because of
	 * extra error handling f.i. you can pass in your own function.
	 *
	 * @constructs
	 * @param see base class
	 * @param {Function} arg.xhr User supplied xml http request function in the
	 *      form of function({String} url, {Function} fn) (optional)
	 * @param {String} arg.dataUrl Url to load the root node(s)
	 */
	initializer: function(arg) {

		if (!arg.anchor) {
			// anchor by default to all sides
			arg.anchor = { right:true,left:true,top:true,bottom:true };
		}

		SUI.TreeView.initializeBase(this, arg);

		// set user specific xhr function if given
		this._xhr = arg.xhr || this._xhr;

		this._dataUrl = arg.dataUrl || null;

		// allow scroll bars in the main div and set the style
		this.el().style.overflow = "auto";
		this.addClass("sui-tv");

		// array of open nodes with indices the ids of the data (not DOM ids)
		this._openNodes = [];
	},

	/**
	 * Height of a node row (CSS, thus including borders and padding)
	 */
	ROW_HEIGHT: 19,

	/**
	 * Height of a node row (CSS, thus including borders)
	 */
	ROW_PADDING_TOP: 1,

	/**
	 * Top and bottom border row width of a node
	 */
	ROW_BORDER_WIDTH: 1,

	/**
	 * Left padding of the text in a node row
	 */
	TEXT_PADDING_LEFT: 2,

	/**
	 * Right padding of the text in a node row
	 */
	TEXT_PADDING_RIGHT: 4,

	/**
	 * Size of the icon (and the indent)
	 */
	ICONS_SIZE: 16,

	/**
	 * Return the data that was selected at the last context menu selection.
	 * Note that this data persist after the context menu is removed and
	 * stays there till the next context menu is requested.
	 * @return {Object} a An object with the current node data (a node
	 *    from the xhr request)
	 */
	contextMenuData: function() {
		return this._contextData;
	},

	/**
	 * Set a custom function for the icons to uses. It operates on the type
	 * set in the data.
	 * @param {Function} fn An function that returns the location of the icon,
	 *     this function has one parameter indicating the icon's type.
	 */
	iconFunction: function(fn) {
		this._iconFunc = fn;
	},

	/**
	 * Load the data into the tree control
	 * @param {Object} arg Object in which the follow entries can be set
	 * - {int[]|string[]} openNodes An object array containing the (data) ids
	 *      of the nodes that are initially open
	 * - {String} dataUrl Url to load the initial node(s)
	 * - {int|string} selected (Data) id of the currently selected node
	 */
	loadData: function(arg) {

		// initially the parent is the main box
		arg.parent = this.el();

		// if there is already a tree rendered, remove it
		if (arg.parent.firstChild) {
			arg.parent.removeChild(arg.parent.firstChild);
		}

		// start at depth 0
		arg.depth = 0;

		// if given, choose the data url from the argument in favor of
		// the one given in the constructor
		arg.dataUrl = this._dataUrl || arg.dataUrl;

		// fill the open nodes array
		if (arg.openNodes) {
			for (var i=0; i<arg.openNodes.length; i++) {
				this._openNodes[arg.openNodes[i]] = true;
			}
		}

		// load the data
		this._loadData(arg);
	},

	/**
	 * onContextMenu event handler: is executed when the user uses the context
	 * menu click on a node.
	 * @param {int} x The x location of the click
	 * @param {int} y The y location of the click
	 */
	onContextMenu: function(x, y) {},

	/**
	 * onSelect event handler: is executed when the user uses selects a node.
	 * Use this to add an action to your selection.
	 */
	onSelect: function() {},

	/**
	 * onSelectionChange event handler: is executed when the selection changes
	 * to another node. Use this to update your GUI (like enabling buttons)
	 */
	onSelectionChange: function() {},

	/**
	 * Transfer the tree selection to the node that was selected by the
	 * context menu. This will also execute the onSelect handler.
	 */
	selectContextMenuNode: function() {
		this._selectNode(this._contextNode, this._contextData);
	},

	/**
	 * Return the data that is currently selected.
	 * @return {Object} a An object with the current node data (a node
	 *    from the xhr request)
	 */
	selectedData: function() {
		return this._selectedData;
	},

	// the currently selected node data
	_selectedData: null,

	// the currently selected HTML node element
	_selectedNode: null,

	// the last (current) selected node data by the context menu
	_contextData: null,

	// the last (current) selected HTML node element by the context menu
	_contextNode: null,

	/* Set the event handlers of a node row.
	 * On the row-div set:
	 *   onclick => _selectNode
	 *   onmouseover => _addHighlight
	 *   onmouseout => _removeHighlight
	 *   oncontextmenu => _handleContextMenu
	 * On the icon set:
	 *   onclick => _openCloseNode
	 * Disable the context menu event on child elements of the row
	 */
	_addEventListeners: function(div, exp, icon, span, arg, dat) {

		// that div, dat, arg are closure variables
		var that = this;

		// Do _selectNode on the click event of a node-row.
		SUI.browser.addEventListener(div, "click",
			function(e) {
				if (!that._selectNode(div, dat)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _addHighlight on the mouseover event of a node-row.
		SUI.browser.addEventListener(div, "mouseover",
			function(e) {
				if (!that._addHighlight(div)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _removeHighlight on the mouseout event of a node-row.
		SUI.browser.addEventListener(div, "mouseout",
			function(e) {
				if (!that._removeHighlight(div)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _handleContextMenu on the contextmenu event of a node-row.
		SUI.browser.addEventListener(div, "contextmenu",
			   function(e) {
				if (!that._handleContextMenu(
						new SUI.Event(this, e), div, dat)) {
					SUI.browser.noPropagation(e);
					that._preventDefault(e);
				}
			}
		);

		// Do _openCloseNode on the click event of the open/close icon.
		if (exp.className.indexOf("sui-tv-exp") !== -1) {
			SUI.browser.addEventListener(exp, "click",
				function(e) {
					if (!that._openCloseNode(div, arg, dat)) {
						SUI.browser.noPropagation(e);
					}
				}
			);
		}

		// Disable the contextmenu event on the two icons and node-row text.
		SUI.browser.addEventListener(exp, "contextmenu", function(e) {
			that._preventDefault(e);
		});
		SUI.browser.addEventListener(span, "contextmenu", function(e) {
			that._preventDefault(e);
		});
		SUI.browser.addEventListener(icon, "contextmenu", function(e) {
			that._preventDefault(e);
		});

	},

	/* Add the highlight to a tree node, except for the selected node
	 */
	_addHighlight: function(div) {
		if (div !== this._selectedNode) {
			// set the highlight + little patch
			SUI.style.addClass(div, "sui-tv-row-highlight");
			this._sizeToNodeContents(div);
		}
	},

	/* Create the list of node-rows for one level of the tree.
	 */
	_createNodeList: function(arg, dat) {
		// create a static UL and append it the the parent
		var ul = SUI.browser.createElement("UL");
		ul.style.position = "static";
		arg.parent.appendChild(ul);

		// if a least one node row as rendered ...
		if (arg.depth) {
			// ... replace the icon of the parent with a wait icon ...
			var ocIcn = arg.parent.firstChild.firstChild;
			ocIcn.nextSibling.src = SUI.imgDir+"/"+ this._iconFunc(arg.pType);
			ocIcn.nextSibling.style.backgroundImage = "none";
			// ... and set the open close node to open
			ocIcn.src = SUI.imgDir+"/"+SUI.resource.tvOpen;
		}

		// loop through all the nodes from the list
		for (var i=0; i<dat.length; i++) {

			// create a static LI
			var li = SUI.browser.createElement("LI");
			li.style.position = "static";

			// create a static DIV for the node row
			var div = SUI.browser.createElement("DIV");
			div.style.position = "static";
			div.style.height =
				(this.ROW_HEIGHT - 2*this.ROW_BORDER_WIDTH) + "px";
			div.style.borderTopWidth = div.style.borderBottomWidth =
				this.ROW_BORDER_WIDTH + "px";
			div.style.borderLeftWidth = div.style.borderRightWidth = "0px";
			div.style.paddingTop = this.ROW_PADDING_TOP + "px";
			div.style.paddingLeft = (arg.depth * this.ICONS_SIZE) + "px";
			SUI.style.addClass(div, "sui-tv-row");

			// create a static image of the icon
			var icon = SUI.browser.createElement("IMG");
			icon.style.position = "static";
			icon.src = SUI.imgDir+"/"+this._iconFunc(dat[i].type);

			// create a static for the open/close icon
			var exp = SUI.browser.createElement("IMG");
			exp.style.position = "static";
			if (dat[i].childListUrl && dat[i].childListUrl !== "") {
				if (this._openNodes[dat[i].id]) {
					exp.src = SUI.imgDir+"/"+SUI.resource.tvOpen;
				} else {
					exp.src = SUI.imgDir+"/"+SUI.resource.tvClosed;
				}
				SUI.style.addClass(exp, "sui-tv-exp");
			} else {
				exp.src = SUI.imgDir+"/"+SUI.resource.tvNone;
			}

			// create a static span for the text
			var span = SUI.browser.createElement("SPAN");
			// TODO refactor: ugly hack
			span.style.cssText = dat[i].style;
			span.style.position = "static";
			span.style.paddingLeft = this.TEXT_PADDING_LEFT + "px";
			span.style.paddingRight = this.TEXT_PADDING_RIGHT + "px";
			span.innerHTML = dat[i].title;

			// append all the items to the DOM UL list
			div.appendChild(exp);
			div.appendChild(icon);
			div.appendChild(span);
			li.appendChild(div);
			ul.appendChild(li);

			// if this is the selected node, select it
			if (dat[i].id == arg.selected) {

				SUI.style.addClass(div, "sui-tv-row-selected");
				this._selectedData = dat[i];

				// was the selection changed (or more likely in this case: on
				// the initial selection) call the onSelectionChange listener
				if (this._selectedNode !== div) {
					this.callListener("onSelectionChange");
				}

				this._selectedNode = div;
			}

			// add the event listers for the node row
			this._addEventListeners(div, exp, icon, span, arg, dat[i]);

			// if the current node is an open node ...
			if (this._openNodes[dat[i].id]) {
				// ... and the node has children ...
				if (dat[i].childListUrl && dat[i].childListUrl !== "") {
					// ... then load the children too
					this._loadData({
						parent: li,
						dataUrl: dat[i].childListUrl,
						pType: dat[i].type,
						depth: arg.depth + 1,
						selected: arg.selected
					});
				}
			}
		}
	},

	/* Store node data when the user uses the context menu and call the
	 * context menu handler.
	 */
	_handleContextMenu: function(e, div, dat)  {
		this._contextNode = div;
		this._contextData = dat;
		this.callListener("onContextMenu", SUI.browser.getX(e.event),
			SUI.browser.getY(e.event));
	},

	/* Default implementation of the icon function.
	 */
	_iconFunc: function(type) {
		return type == 1 ? SUI.resource.tvPage : SUI.resource.tvFolder;
	},

	/* Actual data loading function. Arguments is an object with the following
	 * menbers:
	 * - selected: data id of the selected node
	 * - parent: parent HTML element of the list to create
	 * - dataUrl: location the get the json data for the list
	 * - pType: type op the parent node
	 * - depth: depth of the list
	 */
	_loadData: function(arg) {

		// if there is a row renderd
		if (arg.parent.firstChild) {
			// get a reference to the icon ...
			var prnt = arg.parent.firstChild.firstChild.nextSibling;
			// TODO check this, seem like a bad bug fix
			//if (!prnt) return;
			// ... and set the loading image
			prnt.src = SUI.imgDir+"/"+SUI.resource.tvLoadingAni;
			prnt.style.backgroundImage =
				"url(" + SUI.imgDir + "/" + SUI.resource.tvLoadingBg + ")";
		}

		// load the data and render a list with it
		var that = this;
		this._xhr(arg.dataUrl,
			function(res) {
				that._createNodeList(arg, res.data);
			}
		);
	},

	/* Open or close a node: it is assumed that it has children.
	 */
	_openCloseNode: function(div, a, dat) {

		// if the node was not opened before
		if (!div.nextSibling) {

			// no: open the node: set it in the open nodes array ...
			this._openNodes[dat.id] = true;
			// ... and load the child data.
			this._loadData({
				parent: div.parentNode,
				dataUrl: dat.childListUrl,
				pType: dat.type,
				depth: a.depth + 1
			});

		} else {

			// yes, the node was opened before: toggle the display, icon and
			// entry in the open nodes array.
			if (div.nextSibling.style.display == "none") {

				div.nextSibling.style.display = "block";
				div.firstChild.src = SUI.imgDir+"/"+SUI.resource.tvOpen;
				this._openNodes[dat.id] = true;

			} else {

				div.nextSibling.style.display = "none";
				div.firstChild.src = SUI.imgDir+"/"+SUI.resource.tvClosed;
				this._openNodes[dat.id] = false;

			}
		}
	},

	/* Prevent the default event handling
	 */
	_preventDefault: function(e) {
		if (SUI.browser.isIE) {
			if (!e) {
				e = window.event;
			}
			e.returnValue = false;
		} else {
			e.preventDefault();
		}
	},

	/* Remove highlight to from a tree node
	 */
	_removeHighlight: function(div) {
		// Remove CSS class
		SUI.style.removeClass(div, "sui-tv-row-highlight");
	},

	/* Select a tree node, set highlights and the selected node, then call
	 * the listeners.
	 */
	_selectNode: function(div, dat) {

		// remove the highlight
		SUI.style.removeClass(div, "sui-tv-row-highlight");

		// if there was a previous selection, remove the selection highlight
		if (this._selectedNode) {
			SUI.style.removeClass(this._selectedNode,
				"sui-tv-row-selected");
		}

		// set the selection highlight + little patch
		SUI.style.addClass(div, "sui-tv-row-selected");
		this._sizeToNodeContents(div);

		// store the selection ...
		this._selectedData = dat;
		// ... call the onSelectionChange handler if the selection was changed
		if (div !== this._selectedNode) {
			this.callListener("onSelectionChange");
		}

		// store the selected HTML node and call the select handler
		this._selectedNode = div;
		this.callListener("onSelect");
	},

	/* Set the width of the node div to its content width. This is a patch to
	 * ensure that the background color if the selected node is set to the
	 * width of the div (the offsreen part of a background is often not
	 * rendered when there is a scroll bar).
	 */
	_sizeToNodeContents: function(div) {
		var p = parseInt(div.style.paddingLeft, 10);
		div.style.width = (this.el().scrollWidth -p) + "px";
	},

	/* Default xml-http-request function is the one of the library, but may be
	 * overwritten by a user supplied one
	 */
	_xhr: function(url, cb) {
		SUI.xhr.doGet(url, null, cb);
	}

});