/*
 * GxUI Library 2.0.1
 * Copyright (c) 2009, Artech
 * All rights reserved.
 * 
 * GxUI Library is freely distributable under the terms of the BSD license.
 * 
 */

/// <reference path="..\..\Freezer\Ext\ext-all-dev.js" />

/**
* @class gxui.Treeview
* Tree User Control. Wraps Ext.tree.Panel so it can be used from GeneXus.
*
* This control provides tree-structured UI representation of tree-structured data and is an excellent tool for displaying heirarchical data in an application.
* Tree nodes can be loaded invoking a remote URL or through a SDT.
* To load the tree by invoking a remote URL, set {@link #LazyLoading} = true and set {@link #LoaderURL} to the URL of a procedure that will populate the tree 
* (a main procedure with call protocol = HTTP that returns a collection of gxuiTreeviewNode SDT in Json format). For example: 
* 
*     Treeview.LoaderURL = Procedure.Link()
*
* To load a tree from a SDT, set {@link #LazyLoading} = false and set {@link #Children} to a variable with gxuiTreeviewNode type. The variable might be a collection 
* or a single node with child nodes.
*/
Ext.define('gxui.Treeview', {
	extend: 'gxui.UserControl',

	initialize: function () {
		this.callParent(arguments);

		this.NotifyContext = "false";
		this.NotifyDataType = 'gxuiTreeNode';
		this.CheckedNodes = [];
	},

	// Databinding
	SetChildren: function (data) {
		this.Children = data;
	},

	// Databinding
	GetChildren: function (data) {
		return this.Children;
	},

	// Databinding
	SetCheckedNodes: function (data) {
		this.CheckedNodes = data;
	},

	// Databinding
	GetCheckedNodes: function (data) {
		this.CheckedNodes = [];
		if (this.m_tree) {
			var checkedNodes = this.m_tree.getChecked();
			this.CheckedNodes = Ext.Array.map(checkedNodes, function (node) {
				return node.data.id;
			}, this);
		}
		return this.CheckedNodes
	},

	// Databinding
	SetUncheckedNodes: function (data) {
		this.UncheckedNodes = data;
	},

	// Databinding
	GetUncheckedNodes: function (data) {
		this.UncheckedNodes = [];
		if (this.m_tree) {
			var root = this.m_tree.getRootNode();
			if (root) {
				var nodes = [];
				root.cascadeBy(function () {
					if (this.data.checked === false) {
						nodes.push(this.data.id);
					}
				});
				this.UncheckedNodes = nodes;
			}
		}
		return this.UncheckedNodes;
	},

	// Databinding
	SetDropData: function (data) {
		this.DropData = data;
	},

	// Databinding
	GetDropData: function (data) {
		return this.DropData;
	},

	// Databinding
	SetSelectedNodeData: function (data) {
		this.SelectedNodeData = data;
	},

	// Databinding
	GetSelectedNodeData: function (data) {
		return this.SelectedNodeData;
	},

	// Databinding
	SetSelectedNodes: function (data) {
		this.SelectedNodes = data;
	},

	// Databinding
	GetSelectedNodes: function () {
		return this.SelectedNodes;
	},

	onRender: function () {
		var Tree = Ext.tree;
		var ddGroup = this.DragDropGroup || undefined;

		this.Width = parseInt(this.Width);
		this.Height = parseInt(this.Height);
		this.EnableCheckbox = gxui.CBoolean(this.EnableCheckbox);
		this.LazyLoading = gxui.CBoolean(this.LazyLoading);

		var store = this.createStore();

		var config = {
			id: this.getUniqueId(),
			title: this.Title,
			frame: gxui.CBoolean(this.Frame),
			border: gxui.CBoolean(this.Border) ? undefined : 0,
			cls: this.Cls,
			animate: gxui.CBoolean(this.Animate),
			rootVisible: gxui.CBoolean(this.RootVisible),
			lines: gxui.CBoolean(this.ShowLines),
			store: store,
			folderSort: gxui.CBoolean(this.Sort),
			viewConfig: {},
			plugins: [],
			autoScroll: gxui.CBoolean(this.AutoScroll),
			stateful: gxui.CBoolean(this.Stateful),
			stateId: (this.StateId && this.StateId != "") ? this.StateId : this.getUniqueId(),
			stateEvents: gxui.CBoolean(this.Stateful) ? ['itemcollapse', 'itemexpand'] : undefined,
			getState: gxui.CBoolean(this.Stateful) ? this.getState : undefined,
			applyState: gxui.CBoolean(this.Stateful) ? this.applyState(this.LazyLoading) : undefined,
			listeners: this.getListeners()
		};

		// @TODO: Change this to use AutoWidth and AutoHeight
		if (this.Width != 100)
			config.width = this.Width;
		else
			config.autoWidth = true;

		if (this.Height != 100)
			config.height = this.Height;
		else
			config.autoHeight = true;

		if (gxui.CBoolean(this.EnableDragDrop)) {
			config.viewConfig.plugins = {
				ptype: 'treeviewdragdrop',
				appendOnly: gxui.CBoolean(this.AppendOnly),
				ddGroup: ddGroup
			};
			config.viewConfig.listeners = this.getDDListeners();
		}

		if (gxui.CBoolean(this.Multiselection)) {
			config.selModel = {
				mode: 'MULTI',
				listeners: {
					'selectionchange': function (selModel, nodes) {
						this.SelectedNodes = [];
						Ext.each(nodes, function (node) {
							this.SelectedNodes.push(node.data.id);
						}, this);
					},
					scope: this
				}
			};
		}

		if (gxui.CBoolean(this.Editable)) {
			// The column has to be explicitly defined, to set editor properties
			config.columns = [{
				xtype: 'treecolumn',
				dataIndex: 'text',
				flex: 1,
				editor: {
					xtype: 'textfield',
					allowBlank: false,
					selectOnFocus: true,
					cancelOnEsc: true,
					ignoreNoChange: true
				}
			}];

			config.hideHeaders = true;

			config.plugins.push({
				ptype: 'cellediting',
				pluginId: this.getUniqueId() + '-celledit',
				clicksToEdit: 2,
				listeners: {
					'edit': function (editor, e) {
						this.NodeEditText = e.value;
						if (this.NodeEdit) {
							this.NodeEdit();
						}
					},
					scope: this
				}
			});
			config.viewConfig.toggleOnDblClick = false;
		}

		this.m_tree = Ext.create('Ext.tree.Panel', config);
	},

	onRefresh: function () {
		var selNodes = this.m_tree.getSelectionModel().getSelection();
		if (selNodes && selNodes[0]) {
			this.setSelectedNode(selNodes[0]);
		}
	},

	onAfterRender: function () {
		if (gxui.CBoolean(this.ExpandRoot)) {
			Ext.defer(function () {
				this.m_tree.getRootNode().expand(gxui.CBoolean(this.ExpandAll));
			}, 300, this);
		}
	},

	getUnderlyingControl: function () {
		return this.m_tree;
	},

	addToParent: function () {
		return gxui.CBoolean(this.AddToParentGxUIControl);
	},

	createRootNode: function () {
		return {
			id: (this.RootId ? this.RootId : 'ROOT'),
			text: this.RootText,
			icon: (this.RootIcon ? this.RootIcon : undefined),
			cls: (this.RootCls ? this.RootCls : undefined),
			iconCls: (this.RootIconCls ? this.RootIconCls : undefined),
			draggable: false, // disable root node dragging
			children: !this.LazyLoading ? this.cloneNodes(this.Children) : undefined,
			expanded: gxui.CBoolean(this.ExpandRoot)
		};
	},

	createStore: function () {
		var config = {
			root: this.createRootNode(),
			_enableCheckbox: this.EnableCheckbox
		};


		if (this.LazyLoading) {
			config.proxy = {
				type: 'ajax',
				url: this.LoaderURL,
				reader: {
					type: 'json'
				},
				actionMethods: {
					create: "POST",
					read: "POST",
					update: "POST",
					destroy: "POST"
				}
			};
		}

		return config;
	},

	cloneNodes: function (children) {
		children = gxui.clone(children);

		return children.length && children.length > 0 ? children : [children]
	},

	getRowDropData: function (data) {
		if (data && data.gxRow) {
			var gxGrid = data.gxGrid,
				gxRow = data.gxRow,
				gxCols = data.gxColumns;

			var dropData = {};
			for (var i = 0, len = gxCols.length; i < len; i++) {
				var col = gxCols[i],
					colName = col.gxAttName || (col.gxAttId.charAt(0) == "&" ? col.gxAttId.substring(1) : col.gxAttId),
					cell = gxGrid.getPropertiesCell(data.gxGrid.getUnderlyingControl(), gxRow.id, i, true);
				dropData[colName] = cell.value;
			}

			return dropData;
		}

		return null;
	},

	getNodeById: function (nodeId) {
		return this.m_tree.getStore().getNodeById(nodeId);
	},

	getDDListeners: function () {
		return {
			'dragover': function (node, data, overModel, dropPosition) {
				if (this.NodeOver) {
					// Set UC properties before fireing the event
					this.DropTarget = overModel.data.id;
					this.DropPoint = dropPosition;

					if (data.gxGrid) {
						this.DropData = this.getRowDropData(data);
					}
					else {
						this.DropNode = data.records[0].data.id;
					}

					this.DropAllowed = true;

					/**
					* @event NodeOver
					* Fires when a node is being dragged over another node. While the node is being dragged, an icon is shown describing if the
					* drop operation over the hovered node is allowed. To indicate that the drop operation is allowed (default), set {@link #DropAllowed} = true.
					* To indicate that the drop operation is not allowed, set {@link #DropAllowed} = false.
					* The following properties are set when the event is fired:
					*
					* - {@link #DropTarget}
					* - {@link #DropPoint}
					* - {@link #DropNode}
					*
					*/
					this.NodeOver();

					return this.DropAllowed;
				}
				return true;
			},

			'beforedrop': function (node, data, overModel, dropPosition, opts) {
				if (data.gxGrid) {
					this.DropTarget = overModel.data.id;
					this.DropPoint = dropPosition;

					this.DropData = this.getRowDropData(data);
					opts.cancelDrop();

					if (this.RowDrop) {
						this.RowDrop();
					}
				}
			},

			'drop': function (node, data, overModel, dropPosition) {
				this.DropTarget = overModel.data.id;
				this.DropPoint = dropPosition;

				if (data.records.length > 0) {
					this.DropNode = data.records[0].data.id;
					if (this.NodeDrop) {
						/**
						* @event NodeDrop
						* Fires when a node is dropped.
						* The following properties are set when the event is fired:
						*
						* - {@link #DropTarget}
						* - {@link #DropPoint}
						* - {@link #DropNode}
						*
						*/
						this.NodeDrop();
					}
				}
			},

			scope: this
		};
	},

	getListeners: function () {
		var listeners = {
			'itemclick': function (view, node, item, index, e) {
				var editorPlugin = this.getEditorPlugin();
				var startEdit = (node.data.id == this.SelectedNode) && editorPlugin;
				this.endEdit();
				this.setSelectedNode(node);
				if (!node.data.href) {
					if (this.NotifyContext == "true") {
						this.notifyContext([this.NotifyDataType], { id: node.data.id, text: node.data.text, leaf: node.data.leaf, icon: node.data.icon });
					}
					if (this.Click && (!node.hasChildNodes() || !gxui.CBoolean(this.DisableBranchEvents))) {
						/**
						* @event Click
						* Fires when a node is clicked. The following properties are set when the event is fired:
						*
						* - {@link #SelectedNode}
						* - {@link #SelectedText}
						* - {@link #SelectedIcon}
						* - {@link #SelectedNodeData}
						* - {@link #SelectedNodeChecked}
						*
						*/
						this.Click();
					}
				}
				if (startEdit) {
					editorPlugin.startEdit(node, 0);
				}
			},

			'itemdblclick': function (view, node) {
				this.endEdit();
				this.setSelectedNode(node);
				if (this.DoubleClick && (!node.hasChildNodes() || !gxui.CBoolean(this.DisableBranchEvents))) {
					/**
					* @event DoubleClick
					* Fires when a node is double clicked. The following properties are set when the event is fired:
					*
					* - {@link #SelectedNode}
					* - {@link #SelectedText}
					* - {@link #SelectedIcon}
					* - {@link #SelectedNodeData}
					* - {@link #SelectedNodeChecked}
					*
					*/
					this.DoubleClick();
				}
			},

			'checkchange': function (node) {
				if (this.CheckChange) {
					this.setSelectedNode(node);
					/**
					* @event CheckChange
					* Fires when a node with a checkbox changes its value. The following properties are set when the event is fired:
					*
					* - {@link #SelectedNode}
					* - {@link #SelectedText}
					* - {@link #SelectedIcon}
					* - {@link #SelectedNodeData}
					* - {@link #SelectedNodeChecked}
					*
					*/
					this.CheckChange();
				}
			},

			scope: this
		};

		if (this.ContextMenu) {
			listeners['itemcontextmenu'] = function (view, node) {
				this.endEdit();
				if (this.ContextMenu) {
					this.setSelectedNode(node);
					this.m_tree.getSelectionModel().select(node);
					/**
					* @event ContextMenu
					* Fires when a node is right clicked. The following properties are set when the event is fired:
					*
					* - {@link #SelectedNode}
					* - {@link #SelectedText}
					* - {@link #SelectedIcon}
					* - {@link #SelectedNodeData}
					* - {@link #SelectedNodeChecked}
					*
					*/
					this.ContextMenu();
				}
			};
		}

		return listeners;
	},

	setSelectedNode: function (node) {
		this.SelectedNode = node.data.id;
		this.SelectedText = node.data.text;
		this.SelectedIcon = node.data.icon;
		this.SelectedNodeData = node.data.data;
		this.SelectedNodeChecked = node.data.checked || false;
	},

	getEditorPlugin: function () {
		return this.m_tree.getPlugin(this.getUniqueId() + '-celledit');
	},

	endEdit: function () {
		var editorPlugin = this.getEditorPlugin();
		if (editorPlugin) {
			editorPlugin.completeEdit();
		}
	},

	getState: function () {
		var nodes = [], state = Ext.tree.Panel.prototype.getState.apply(this, arguments);
		this.getRootNode().eachChild(function (child) {
			//function to store state of tree recursively
			var storeTreeState = function (node, expandedNodes) {
				if (node.isExpanded() && node.childNodes.length > 0) {
					expandedNodes.push(node.getPath("id"));
					node.eachChild(function (child) {
						storeTreeState(child, expandedNodes);
					});
				}
			};
			storeTreeState(child, nodes);
		});

		Ext.apply(state, {
			expandedNodes: nodes
		});

		return state;
	},

	applyState: function (lazyLoading) {
		return function (state) {
			var nodes = state.expandedNodes || [],
				len = nodes.length;

			var expandNodes = Ext.bind(function () {
				for (var i = 0; i < len; i++) {
					if (typeof nodes[i] != 'undefined') {
						this.expandPath(nodes[i], 'id');
					}
				}
				Ext.tree.Panel.prototype.applyState.call(this, state)
			}, this);
			
			this.collapseAll();
			if (lazyLoading) {
				setTimeout(expandNodes, 100);
			}
			else {
				expandNodes();
			}
		};
	},

	findChildNode: function (id, nodes) {
		for (var i = 0, len = nodes.length; i < len; i++) {
			if (nodes[i].id == id) {
				return nodes[i];
			}
			if (nodes[i].children.length > 0) {
				var node = this.findChildNode(id, nodes[i].children);
				if (node) {
					return node;
				}
			}
		}
		return null;
	},

	setNodeProperty: function (nodeId, name, value) {
		var node = this.getNodeById(nodeId);
		if (node) {
			node.set(name, value);
		}
	},

	methods: {
		// Methods
		/**
		* Selects a node by id.
		* @param {String} nodeId Node id
		* @method
		*/
		SelectNode: function (nodeId) {
			var node = this.getNodeById(nodeId);
			if (node) {
				this.setSelectedNode(node)
				this.m_tree.getSelectionModel().select(node);
				this.m_tree.expandPath(node.getPath("id"), "id");
			}
		},

		/**
		* Selects the node immediately following the currently selected node.
		* @method
		*/
		SelectNextNode: function () {
			this.m_tree.getSelectionModel().selectNext();
		},

		/**
		* Selects the node that precedes the currently selected node.
		* @method
		*/
		SelectPreviousNode: function () {
			this.m_tree.getSelectionModel().selectPrevious();
		},

		/**
		* Remove a node from the tree
		* @param {String} nodeId Node id
		* @method
		*/
		DeleteNode: function (nodeId) {
			var node = this.getNodeById(nodeId);
			if (node) {
				node.remove();
			}
		},

		/**
		* Expand a node by id.
		* @param {String} nodeId Node id
		* @method
		*/
		ExpandNode: function (nodeId) {
			var node = this.getNodeById(nodeId);
			if (node) {
				node.expand();
			}
		},

		/**
		* Expand all the tree nodes.
		* @method
		*/
		ExpandAllNodes: function () {
			this.m_tree.expandAll();
		},

		/**
		* Collapse a node by id.
		* @param {String} nodeId Node id
		* @method
		*/
		CollapseNode: function (nodeId) {
			var node = this.getNodeById(nodeId);
			if (node) {
				node.collapse();
			}
		},

		/**
		* Collapse all the tree nodes.
		* @method
		*/
		CollapseAllNodes: function () {
			this.m_tree.collapseAll();
		},

		/**
		* Reloads the tree from a given node. If a node is not provided, it reloads the tree from the root node.
		* If {@link #LazyLoading} = false, the tree is always reloaded from the root node, reading the value from {@link #Children} property.
		* @param {String} [nodeId] Node id
		* @method
		*/
		Reload: function (node, expand) {
			var tree = this.m_tree;
			// node can be a TreeNode or a String with the Id of a node. If node is undefined, the root node is reloaded.
			var n = node ? ((typeof node == 'object') ? node : this.getNodeById(node)) : tree.getRootNode();

			if (n) {
				var loadCallback = function (node, initState) {
					node = node || n;
					if (expand || expand === undefined) {
						node.expand();
					}
					if (initState !== false) {
						tree.initState();
					}
				};

				var store = tree.getStore();
				if (this.LazyLoading) {
					var loadCfg = {
						callback: Ext.bind(loadCallback, this),
						node: n
					};

					store.getProxy().url = this.LoaderURL;
					if (store.isLoading())
						Ext.defer(store.load, 500, store, [loadCfg]);
					else
						store.load(loadCfg);
				}
				else {
					if (n == tree.getRootNode()) {
						tree.setRootNode(this.createRootNode());
					}
					else {
						var rawNode = this.findChildNode(node, this.Children),
							newNode = n.parentNode.replaceChild(rawNode, n);
					}
					loadCallback(newNode, false);
				}

				if (this.SelectedNode != undefined) {
					this.SelectNode(this.SelectedNode);
				}
			}
		},

		/**
		* Reloads the tree from the root node and applies {@link #Width}, {@link #Height} and {@link #Title} properties.
		* @method
		*/
		Refresh: function () {
			var tree = this.m_tree;
			tree.setHeight((this.Height != 100) ? this.Height : undefined);
			tree.setWidth((this.Width != 100) ? this.Width : undefined);
			tree.setTitle(this.Title);
			this.Reload(tree.getRootNode(), gxui.CBoolean(this.ExpandRoot));
		},

		/**
		* Shows the control
		* @method
		*/
		Show: function () {
			this.m_tree.setVisible(true);
		},

		/**
		* Hides the control
		* @method
		*/
		Hide: function () {
			this.m_tree.setVisible(false);
		},

		/**
		* Returns the id of the parent of the given node. If the given node doesn't exist or is the root node, it returns "".
		* @param {String} nodeId Node id
		* @return {String}
		* @method
		*/
		GetNodeParentId: function (nodeId) {
			var node = this.getNodeById(nodeId);
			if (node && node.parentNode) {
				return node.parentNode.data.id;
			}
			return "";
		},

		/**
		* Sets the data property of the given node with nodeData.
		* @param {String} nodeId Node id
		* @param {Object} nodeData Node data to set in the data property of the node. It can be any type of SDT.
		* @method
		*/
		SetNodeData: function (nodeId, nodeData) {
			var node = this.getNodeById(nodeId);
			if (node) {
				node.data.data = nodeData;
			}
		},

		/**
		* Returns the data property of the given node.
		* @param {String} nodeId Node id
		* @return {Object}
		* @method
		*/
		GetNodeData: function (nodeId) {
			var node = this.getNodeById(nodeId);
			if (node) {
				return node.data.data;
			}
		},

		/**
		* Sets the text of a given node.
		* @param {String} nodeId Node id
		* @param {String} text New text for the node
		* @method
		*/
		SetNodeText: function (nodeId, text) {
			this.setNodeProperty(nodeId, 'text', text);
		},


		/**
		* Sets the boolean value of a property of a given node.
		* @param {String} nodeId Node id
		* @param {String} name Property name
		* @param {Boolean} value Property value
		* @method
		*/
		SetNodePropertyBoolean: function (nodeId, name, value) {
			this.setNodeProperty(nodeId, name, value);
		},

		/**
		* Sets the string value of a property of a given node.
		* @param {String} nodeId Node id
		* @param {String} name Property name
		* @param {String} value Property value
		* @method
		*/
		SetNodePropertyString: function (nodeId, name, value) {
			this.setNodeProperty(nodeId, name, value);
		},

		/**
		* Sets the numeric value of a property of a given node.
		* @param {String} nodeId Node id
		* @param {String} name Property name
		* @param {Number} value Property value
		* @method
		*/
		SetNodePropertyNumber: function (nodeId, name, value) {
			this.setNodeProperty(nodeId, name, value);
		},

		/**
		* Starts editing a given node
		* @param {String} nodeId Node id
		* @param {Number} [value] A value to initialize the node editor with.
		* @method
		*/
		StartEdit: function (nodeId, value) {
			var node = this.getNodeById(nodeId),
				editorPlugin;
			if (node) {
				editorPlugin = this.getEditorPlugin();
				if (editorPlugin) {
					if (value !== undefined) {
						editorPlugin.on({
							'beforeedit': function (editor, e) {
								e.value = value;
							},
							single: true
						});
					}
					editorPlugin.startEdit(node, 0);
				}
			}
		},

		/**
		* Cancels any active editing.
		* @method
		*/
		CancelEdit: function () {
			var editorPlugin = this.getEditorPlugin();
			if (editorPlugin) {
				editorPlugin.cancelEdit();
			}
		},

		/**
		* Sets the text of a given node.
		* @param {String} nodeId Node id
		* @param {String} text New text for the node
		* @method
		*/
		ClearAllNodes: function () {
			var root = this.m_tree.getRootNode();
			root.removeAll();
			delete root.data.children;
		},

		/**
		* Returns true if the given node exists in the tree.
		* @param {String} nodeId Node id
		* @return {Boolean}
		* @method
		*/
		NodeExists: function (nodeId) {
			var node = this.getNodeById(nodeId);
			return (node ? true : false);
		}
	}
});

// isValidDropPoint is overriden to be able to fire dragover event.
Ext.tree.ViewDropZone.override({
	isValidDropPoint: function (node, position, dragZone, e, data) {
		if (this.callOverridden(arguments) === false)
			return false;

		if (this.view.fireEvent('dragover', node, data, this.view.getRecord(node), position, e) === false)
			return false;

		return true;
	}
});

Ext.data.TreeStore.override({
	fillNode: function (node, records) {
		if (records) {
			for (var i = 0, len = records.length; i < len; i++) {
				var record = records[i];
				if (!this._enableCheckbox) {
					delete record.raw.checked;
					record.data.checked = null;
				}
				if (record.raw.leaf === false && record.raw.children && record.raw.children.length == 0)
					delete record.raw.children;
			}
		}
		return this.callOverridden(arguments);
	}
});