/**
 * eGroupWare egw_action framework - egw action framework
 *
 * @link http://www.egroupware.org
 * @author Andreas Stöckel <as@stylite.de>
 * @copyright 2011 by Andreas Stöckel
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package egw_action
 * @version $Id$
 */

/*egw:uses
	egw_action;
	egw_action_common;
	egw_grid_columns;
*/

/** -- egwGridDataElement Class -- **/

var EGW_DATA_TYPE_RANGE = 0;
var EGW_DATA_TYPE_ELEMENT = 1;

/**
 * Contains the data (model) objects which retrieve data from the given source and
 * pass it to.
 *
 * @param object _parent the parent data element in which the new element is contained
 * @param object _columns the columns object which contains information about the data columns
 * @param object _readQueue is the queue object which queues data-fetching calls and executes these
 * 	asynchronously.
 * @param object _objectManager if this element is the root element (_parent is null),
 * 	specify the _objectManager in order to supply a parent object manager for that
 * 	element.
 */
function egwGridDataElement(_id, _parent, _columns, _readQueue, _objectManager)
{
	// Copy the passed arguments
	this.id = _id;
	this.parent = _parent;
	this.columns = _columns;
	this.readQueue = _readQueue;

	// Generate the action object associated to this element
	this.parentActionObject = _parent ? _parent.actionObject : _objectManager;
	this.actionObject = null;

	// If this is the root object, add the an root action object to the objectManager
	if (!_parent)
	{
		this.actionObject = this.parentActionObject.addObject(_id, null,
			EGW_AO_FLAG_IS_CONTAINER | EGW_AO_FLAG_DEFAULT_FOCUS);
		this.readQueue.setDataRoot(this);
	}

	// Preset some parameters
	this.children = [];

	this.data = {};
	this.caption = false;
	this.iconUrl = false;
	this.iconSize = false;
	this.iconOverlay = [];
	this.opened = _parent == null;
	this.index = 0;
	this.canHaveChildren = false;
	this.type = egwGridViewRow;
	this.userData = null;
	this.updatedGrid = null;
	this.actionLinkGroups = {};
	this.group = false;
	this.capColTime = 0;
	this.rowClass = "";

	this.gridViewObj = null;
}

var EGW_GRID_DATA_UPDATE_TIME = 0;

egwGridDataElement.prototype.free = function()
{
	//TODO
}

egwGridDataElement.prototype.set_rowClass = function(_value)
{
	if (_value != this.rowClass)
	{
		this.rowClass = _value;
	}
}

egwGridDataElement.prototype.set_caption = function(_value)
{
	if (_value != this.caption)
	{
		this.capColTime = EGW_GRID_DATA_UPDATE_TIME;
		this.caption = _value;
	}
}

egwGridDataElement.prototype.set_iconUrl = function(_value)
{
	if (_value != this.iconUrl)
	{
		this.capColTime = EGW_GRID_DATA_UPDATE_TIME;
		this.iconUrl = _value;
	}
}

egwGridDataElement.prototype.set_iconOverlay = function(_value)
{
	if (!egwArraysEqual(_value, this.iconOverlay))
	{
		this.capColTime = EGW_GRID_DATA_UPDATE_TIME;
		this.iconOverlay = _value;
	}
}

egwGridDataElement.prototype.set_iconSize = function(_value)
{
	if (_value != this.iconSize)
	{
		this.capColTime = EGW_GRID_DATA_UPDATE_TIME;
		this.iconSize = _value;
	}
}

egwGridDataElement.prototype.set_opened = function(_value)
{
	this.opened = _value;
}

egwGridDataElement.prototype.set_canHaveChildren = function(_value)
{
	// Calculate the canHaveChildren value which would really be set
	var rv = _value && (this.children.length == 0);

	if (rv != this.canHaveChildren)
	{
		this.canHaveChildren = _value;
		this.capColTime = EGW_GRID_DATA_UPDATE_TIME;
	}
}

egwGridDataElement.prototype.set_group = function(_value)
{
	this.group = _value;

	var root = this.getRootElement();

	var groups = _value.split(",");
	for (var key in groups)
	{
		var val = groups[key];
		if (typeof root.actionLinkGroups[val] != "undefined")
		{
			this.actionObject.updateActionLinks(root.actionLinkGroups[val]);
		}
	}
}

/**
 * Updates the column data. The column data is an object (used as associative array)
 * which may be of the following outline:
 *
 * 	{
 * 		"[col1_id]": "[data]",
 * 		"[col2_id]":
 * 			{
 * 				"data": "[data]",
 * 				"sortData": "[sortData]"
 * 			}
 * 	}
 *
 * "sortData" is data which is used for sorting instead of "data" when set.
 */
egwGridDataElement.prototype.set_data = function(_value)
{
	if (typeof _value == "object" && _value.constructor == Object)
	{
		// Update the column data specified in the value
		for (col_id in _value)
		{
			var val = _value[col_id];

			var data = "";
			var sortData = null;

			if (typeof val == "object")
			{
				data = typeof val.data != "undefined" ? val.data : "";
				sortData = typeof val.sortData != "undefined" ? val.sortData : null;
			}
			else
			{
				data = val;
			}

			// Set the data column timestamp - this is used inside the grid_view
			// row unit in order to only update data which has really changed
			var ts = 0;
			var newData = true;

			if (typeof this.data[col_id] != "undefined" && this.data[col_id].data == data)
			{
				ts = this.data[col_id].ts;
				newData = false;
			}

			if (newData)
			{
				ts = EGW_GRID_DATA_UPDATE_TIME;
			}

			this.data[col_id] = {
				"data": data,
				"sortData": sortData,
				"queued": false,
				"time": ts
			}
		}
	}
}

egwGridDataElement.prototype.addClass = function(_value) {
	// Split the string into individual classes
	classes = this.rowClass.split(' ');

	// Add the given class if it is not already set
	if (classes.indexOf(_value) == -1)
	{
		classes.push(_value);

		// Write the new value
		this.rowClass = classes.join(' ');

		// Update the grid element
		this.callGridViewObjectUpdate();
	}
}

egwGridDataElement.prototype.removeClass = function(_value) {
	// Split the string into individual classes
	classes = this.rowClass.split(' ');

	// Remove the given value if it exists
	var idx = classes.indexOf(_value);
	if (idx > -1)
	{
		classes.splice(idx, 1);

		// Write the new value
		this.rowClass = classes.join(' ');

		// Update the grid element
		this.callGridViewObjectUpdate();
	}
}


/**
 * Loads data into the GridData element. This function has two basic operating modes:
 * 
 * 1. If an array of objects is passed, the specified objects are added as children.
 * If a child node with the given ID already exists, it is updated.
 * The given data array must have the following form:
 * [
 * 	{
 * 		["entryType": (EGW_DATA_TYPE_ELEMENT | EGW_DATA_TYPE_RANGE)] // Defaults to EGW_DATA_TYPE_ELEMENT
 *		"type": "[Typeclass]" // Typeclass of the view-container: specifies the chars after the egwGridView-prefix. Defaults to "Row" which becomes "egwGridViewRow",
 * 		IF EGW_DATA_TYPE_ELEMENT:
 *			"id": [ Name of the element ]
 * 			"children": [ Objects which will be added to the children of the element ]
 *			ELEMENT DATA // See below
		IF EGW_DATA_TYPE_RANGE:
			"count": [Count of Elements], | "ids": [ Array with element ids ],
			"group": [ Action Link Group to which the generated objects should be added ]
			"prefix": "[String prefix which will be added to each element including their index in the list]"
 * 	}
 * ]
 *
 * 2. If a string or number is passed, inside an array, it is encapsulated into
 * an empty entry with that id
 *
 * 3. If an object with element dara is passed, the properties of the element will
 * be updated to the given values.
 *
 * {
 * 		"data": { COLUMN DATA OBJECT } // See "set_data" function
 * 		"caption": "[Caption]" // Used in the EGW_COL_TYPE_NAME_ICON_FIXED column
 * 		"iconUrl": "[IconUrl]" // Used in the EGW_COL_TYPE_NAME_ICON_FIXED column
 * 		"opened": [true|false] // Specifies whether the row is "opened" or "closed" (in trees)
 * 		"canHaveChildren": [true|false] // Specifies whether the row "open/close" button is displayed
 * }
 */
egwGridDataElement.prototype.loadData = function(_data, _doCallUpdate)
{
	// Store the current timestamp
	EGW_GRID_DATA_UPDATE_TIME = (new Date).getTime();

	if (typeof _doCallUpdate == "undefined")
	{
		_doCallUpdate = false;
	}

	if (_data.constructor == Array)
	{
		var virgin = this.children.length == 0;
		var last_element = null;

		for (var i = 0; i < _data.length; i++)
		{
			var entry = _data[i];

			// Single string entries are automatically converted to an entry
			// with that id
			if (typeof entry == String || typeof entry == Number)
			{
				entry = {
					"id": (entry + '') // The "+ ''" converts the entry to a string
				}
			}

			if (entry.constructor != Object)
			{
				continue;
			}

			var element = null;

			// Read the entry type and the element type (if they are set)
			var entryType = typeof entry.entryType == "number" ? entry.entryType :
				EGW_DATA_TYPE_ELEMENT;
			var type = (typeof entry.type == "string") && (typeof window["egwGridView" + entry.type] == "function") ?
				window["egwGridView" + entry.type] : egwGridViewRow;

			// Inserts a range of given dummy elements into the data tree
			if (entryType == EGW_DATA_TYPE_RANGE)
			{
				var prefix = typeof entry.prefix == "string" ? entry.prefix : "elem_";
				var canHaveChildren = typeof entry.canHaveChildren == "boolean" ? entry.canHaveChildren : false;
				var index = last_element ? last_element.index + 1 : 0;
				var group = typeof entry.group == "string" ? entry.group : false;
				var ids = [];

				if (typeof entry.ids != "undefined")
				{
					ids = entry.ids;
				}
				else if (typeof entry.count != "undefined")
				{
					var count = typeof entry.count == "number" && entry.count >= 0 ? entry.count : 1;

					for (var j = 0; j < count; j++)
					{
						ids.push(prefix + (index + j));
					}
				}

				for (var j = 0; j < ids.length; j++)
				{
					element = this.insertElement(index + j, ids[j]);
					element.type = type; // Type can only be set directly after creation
					element.canHaveChildren = canHaveChildren;
					if (group !== false)
					{
						element.set_group(group);
					}
				}
			}
			else if (entryType == EGW_DATA_TYPE_ELEMENT)
			{
				var id = typeof entry.id == "string" ? entry.id : "";
				element = null;

				if (!virgin && id)
				{
					element = this.getElementById(id, 1);
				}

				if (!element)
				{
					element = this.insertElement(false, id);
					element.type = type; // Type can only be set directly after creation
				}


				element.loadData(entry);
			}

			last_element = element;
		}
	}
	else
	{
		// Load all the data element for which a setter function exists
		egwActionStoreJSON(_data, this, true);

		// Load the child data
		if (typeof _data.children != "undefined" && _data.children.constructor == Array)
		{
			this.loadData(_data.children);
		}

		if (_doCallUpdate)
		{
			this.callBeginUpdate();
		}

		this.callGridViewObjectUpdate();
	}
}

/**
 * Resets all relevant data (the column data, icon and icon size) of the element
 * and triggers a gridViewObj update.
 */
egwGridDataElement.prototype.clearData = function()
{
	this.data = {};
	this.caption = false;
	this.iconUrl = false;
	this.iconSize = false;
	this.capColTime = 0;

	this.callGridViewObjectUpdate();
}

/**
 * Inserts a new element as child at the given position
 *
 * @param integer _index is the index at which the element will be inserted. If 
 * 	false, the element will be added to the end of the list.
 * @param string _id is the id of the newly created element
 * @returns the newly created element
 */
egwGridDataElement.prototype.insertElement = function(_index, _id)
{
	if (!_index)
	{
		_index = this.children.length;
	}
	else
	{
		_index = Math.max(0, Math.min(this.children.length, _index));
	}

	// Create the data element
	var element = new egwGridDataElement(_id, this, this.columns, this.readQueue,
		null);
	element.index = _index;

	// Create the action object with a temporary AOI which can be used to make
	// the object visible
	var object = this.actionObject.insertObject(_index, _id,
		new egwGridTmpAOI(this.getRootElement().gridObject, _index), 0);
	object.data = element;

	// Link the two together
	element.actionObject = object;

	// As this element now at least has one child, "canHaveChildren" must be true
	this.canHaveChildren = true;

	// Insert the element at the given index
	this.children.splice(_index, 0, element);

	// Increment the index of all following elements
	for (var i = _index + 1; i < this.children.length; i++)
	{
		this.children[i].index++;
	}

	return element;
}

/**
 * Adds a new data element as child to the end of the list.
 *
 * @param string _id is the object identifier
 * @returns the newly created element
 */
egwGridDataElement.prototype.addElement = function(_id)
{
	return this.insertElement(false, _id);
}

egwGridDataElement.prototype.removeElement = function()
{
	//TODO
}

/**
 * Searches for the element with the given id and returns it. _depth specifies
 * the maximum recursion depth. May be omited.
 */
egwGridDataElement.prototype.getElementById = function(_id, _depth)
{
	if (typeof _depth == "undefined")
	{
		_depth = -1;
	}

	// Check whether this element is the searched one, if yes return it
	if (_id == this.id)
	{
		return this;
	}

	// Only continue searching in deeper levels, if the given depth is greater than
	// zero, or hasn't been defined and is therefore smaller than zero
	if (_depth < 0 || _depth > 0)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			var elem = this.children[i].getElementById(_id, _depth - 1);

			if (elem)
			{
				return elem;
			}
		}
	}

	return null;
}

/**
 * Returns all children as array - this list will be used to set the item list
 * of the egwGridViewSpacer containers.
 */
egwGridDataElement.prototype.getChildren = function(_callback, _context)
{
	if (this.children.length > 0)
	{
		_callback.call(_context, this.children, true);
	}
	else if (this.canHaveChildren)
	{
		// If the children havn't been loaded yet, request them via queue call.
		this.readQueue.queueCall(this, EGW_DATA_QUEUE_CHILDREN, function() {
			_callback.call(_context, this.children, false);
		}, this);
	}
}

egwGridDataElement.prototype.hasColumn = function(_columnId, _returnData)
{
	// Get the column
	var col = this.columns.getColumnById(_columnId);
	var res = null;

	if (col)
	{
		res = false;

		// Check whether the queried column is the "EGW_COL_TYPE_NAME_ICON_FIXED" column
		if (col.type == EGW_COL_TYPE_NAME_ICON_FIXED)
		{
			if (this.caption !== false)
			{
				if (_returnData)
				{
					res = {
						"caption": this.caption,
						"iconUrl": this.iconUrl,
						"time": this.capColTime,
						"queued": false
					}
				}
				else
				{
					res = true;
				}
			}

			if (!_returnData && typeof (this.data[_columnId]) != "undefined" && this.data[_columnId].queued)
			{
				res = true;
			}
		}
		else if (col.type == EGW_COL_TYPE_CHECKBOX)
		{
			if (!_returnData)
			{
				res = true; // Tell the loader that the checkbox data is always available
			}
			else
			{
				var dataSet = (typeof this.data[_columnId] != "undefined");
				res = {
					"data": dataSet ? this.data[_columnId].data : 0,
					"time": dataSet ? this.data[_columnId].time : this.capColTime,
					"queued": false
				}
			}
		}
		else
		{
			// Check whether the column data of this column has been read,
			// if yes, return it.
			if (typeof this.data[_columnId] != "undefined")
			{
				if (_returnData)
				{
					// If the data should be returned, simply return it
					if (typeof this.data[_columnId].data != "undefined")
					{
						res = this.data[_columnId];
					}
				}
				else
				{
					// Otherwise check whether the data has been loaded - if this
					// is the case, return true
					if (typeof this.data[_columnId].data != "undefined")
					{
						res = true;
					}
					else
					{
						// Otherwise return the "queued" state
						res = this.data[_columnId].queued;
					}
				}
			}
			// Probably there is a default value specified for this column...
			else if (col["default"] !== EGW_COL_DEFAULT_FETCH)
			{
				if (_returnData)
				{
					res = col["default"];
				}
				else
				{
					res = true;
				}
			}
		}
	}

	return res;
}

/**
 * Returns the data for the given columns or incomplete data if those columns
 * are not available now. Those columns are loaded asynchronously in the background
 * and the GridViewObject is informed about this as soon as the new data has been
 * loaded.
 *
 * @param _columnIds is an array of column ids for which the data should be returned
 */
egwGridDataElement.prototype.getData = function(_columnIds)
{
	var queryList = [];
	var result = {};

	for (var i = 0; i < _columnIds.length; i++)
	{
		var res = this.hasColumn(_columnIds[i], true);

		// Either add the result to the result list (if the column data was available)
		// or add it to the query list.
		if (res !== null)
		{
			if (res !== false)
			{
				if (typeof res.queued != "undefined" && res.queued != false)
				{
					result[_columnIds[i]] = false;
				}
				else
				{
					result[_columnIds[i]] = res;
				}
			}
			else
			{
				queryList.push(_columnIds[i]);
			}
		}
	}

	// If one data entry hasn't been available, queue the request for this data
	// in the readQueue
	if (queryList.length > 0)
	{
		this.readQueue.queueCall(this, queryList);
	}

	return result;
}


/**
 * Calls the row object update function - checks whether the row object implements
 * this interface and whether it is set.
 */
egwGridDataElement.prototype.callGridViewObjectUpdate = function(_immediate)
{
	if (typeof _immediate == "undefined")
	{
		_immediate = false;
	}

	if (this.gridViewObj && typeof this.gridViewObj.doUpdateData == "function")
	{
		this.gridViewObj.doUpdateData(_immediate);
	}
}

egwGridDataElement.prototype.getDeepestOpened = function()
{
	if (this.opened && this.children.length > 0)
	{
		return this.children[this.children.length - 1].getDeepestOpened();
	}
	else
	{
		return this;
	}
}

/**
 * Returns whether this data element is a odd or even one
 */
egwGridDataElement.prototype.isOdd = function()
{
	return (this.index % 2) == 0; // Improve - old exact version needed way too much performance
}

/**
 * Function which is called by the grid view container in order to update the 
 * action object aoi.
 */
egwGridDataElement.prototype.setGridViewObj = function(_obj)
{
	this.gridViewObj = _obj;

	if (_obj && typeof _obj.getAOI == "function")
	{
		var aoi = _obj.getAOI();
		this.actionObject.setAOI(aoi);
		aoi.reconnectActions();
	}
	else
	{
		this.actionObject.setAOI(null);
	}
}

/**
 * Returns the root element
 */
egwGridDataElement.prototype.getRootElement = function()
{
	if (!this.parent)
	{
		return this;
	}
	else
	{
		return this.parent.getRootElement();
	}
}

/**
 * Returns the depth of this element in the document tree
 */
egwGridDataElement.prototype.getDepth = function()
{
	return (this.parent) ? (this.parent.getDepth() + 1) : 0;
}

/**
 * Calls the beginUpdate function of the grid associated to the grid view object
 */
egwGridDataElement.prototype.callBeginUpdate = function()
{
	if (this.gridViewObj)
	{
		var root = this.getRootElement();

		if (root.updatedGrid != this.gridViewObj.grid)
		{
			if (root.updatedGrid)
			{
				root.updatedGrid.endUpdate();
			}
			root.updatedGrid = this.gridViewObj.grid;
			root.updatedGrid.beginUpdate();
		}
	}
}

/**
 * Calls the end update function of the currently active updated grid
 */
egwGridDataElement.prototype.callEndUpdate = function()
{
	var root = this.getRootElement();

	if (root.updatedGrid)
	{
		root.updatedGrid.endUpdate();
		root.updatedGrid = null;
	}
}

/**
 * Deletes all child elements
 */
egwGridDataElement.prototype.empty = function()
{
	this.actionObject.clear();
	this.children = [];

	// Prevent all event handlers which are associated to elements in the read
	// queue from being called - those elements might no longer exist
	this.readQueue.flushEventQueue();
}

/**
 * Returns all parents in a list
 */
egwGridDataElement.prototype.getParentList = function(_lst)
{
	if (typeof _lst == "undefined")
	{
		_lst = [];
	}

	_lst.push(this);

	if (this.parent)
	{
		this.parent.getParentList(_lst);
	}

	return _lst;
}

/**
 * Requires a certain column to have all data localy - if this isn't the case,
 * the data is fetched from the server.
 *
 * @param string _colId specifies the column which should be loaded
 * @param function _callback is the function which should be called once all data
 * 	has been fetched.
 * @param object _context is the context in which the callback should be executed
 * @param object _loadIds is used internally to accumulate the object ids which
 * 	should be loaded.
 */
egwGridDataElement.prototype.requireColumn = function(_colId, _callback, _context,
	_loadElems)
{
	var outerCall = false;
	if (typeof _loadElems == "undefined")
	{
		_loadElems = {
			"elems": []
		}
		outerCall = true;
	}

	if (!outerCall && !this.hasColumn(_colId, false))
	{
		_loadElems.elems.push(this);
	}

	for (var i = 0; i < this.children.length; i++)
	{
		this.children[i].requireColumn(_colId, null, null, _loadElems);
	}

	// TODO: In which cases has this to be aborted?
	if (outerCall)
	{
		if (_loadElems.elems.length > 0)
		{
			this.readQueue.queueCall(_loadElems.elems, [_colId], function() {
				_callback.call(_context);
			}, null);
		}
		else
		{
			// If all elements had been loaded, postpone calling the callback function
			window.setTimeout(function() {
				_callback.call(_context);
			}, 0);
		}
	}
}


/**
 * Sorts the data element by the given column, the given sort direction and the
 * given sort mode - if the tree doesn't have all the column data loaded which is
 * needed for sorting, it first queries it from the server.
 *
 * @param string _colId is the id of the column
 * @param int _dir is one of EGW_COL_SORTMODE_*
 * @param int _mode is one of EGW_COL_SORTABLE_*
 * @param function _callback is a callback function which is called once the
 * 	sorting is done
 * @param 
 * @param boolean _outerCall is used internally, do not specify it
 */
egwGridDataElement.prototype.sortChildren = function(_colId, _dir, _mode, _callback,
	_context, _outerCall)
{
	if (typeof _outerCall == "undefined")
	{
		_outerCall = true;
	}

	// If this is the outer call of the function, we first have to make sure
	// that all data for the given column id is available
	if (_outerCall)
	{
		this.requireColumn(_colId, function() {
			// Call the actual sort part of this function by explicitly passing "false"
			// to the _outerCall parameter
			this.sortChildren(_colId, _dir, _mode, _callback, _context, false);

			_callback.call(_context);
		}, this);
	}
	else
	{
		// Select the sort function
		var sortFunc = null;
		switch (_mode) {
			case EGW_COL_SORTABLE_ALPHABETIC:
				sortFunc = egwGridData_sortAlphabetic;
				break;

			case EGW_COL_SORTABLE_NATURAL:
				sortFunc = egwGridData_sortNatural;
				break;

			case EGW_COL_SORTABLE_NUMERICAL:
				sortFunc = egwGridData_sortNumerical;
				break;
		}

		var col = this.columns.getColumnById(_colId);

		// Determine the mode multiplier which is used to sort the list in the
		// given direction.
		var dirMul = (_dir == EGW_COL_SORTMODE_ASC) ? 1 : -1;
		this.children.sort(function (a, b) {
			// Fetch the sortData or the regular data from the a and b element
			// and pass it to the sort function
			var aData = "";
			var bData = "";

			switch (col.type)
			{
				case EGW_COL_TYPE_DEFAULT:
					aData = a.data[_colId].sortData === null ? a.data[_colId].data :
						a.data[_colId].sortData;
					bData = b.data[_colId].sortData === null ? b.data[_colId].data :
						b.data[_colId].sortData;
					break;

				case EGW_COL_TYPE_NAME_ICON_FIXED:
					aData = a.caption;
					bData = b.caption;
					break;

				case EGW_COL_TYPE_CHECKBOX:
					aData = a.actionObject.getSelected() ? 1 : 0;
					bData = b.actionObject.getSelected() ? 1 : 0;
					break;
			}

			return sortFunc(aData, bData) * dirMul;
		});

		// Sort all children
		for (var i = 0; i < this.children.length; i++)
		{
			this.children[i].sortChildren(_colId, _dir, _mode, null, null, false);
		}

		// Sorting is done - call the callback function
		if (_callback)
		{
			_callback.call(_context);
		}
	}
}

function egwGridData_sortAlphabetic(a, b)
{
	return (a > b) ? 1 : -1;
}

function egwGridData_sortNumerical(a, b)
{
	aa = parseFloat(a);
	bb = parseFloat(b);

	return (aa > bb) ? 1 : -1;
}

/**
 * See http://my.opera.com/GreyWyvern/blog/show.dml/1671288
 */
function egwGridData_sortNatural(a, b)
{
	function chunkify(t)
	{
		var tz = [], x = 0, y = -1, n = 0, i, j;

		while (i = (j = t.charAt(x++)).charCodeAt(0))
		{
			var m = (i == 46 || (i >=48 && i <= 57));
			if (m !== n)
			{
				tz[++y] = "";
				n = m;
			}
			tz[y] += j;
		}

		return tz;
	}

	var aa = chunkify(a);
	var bb = chunkify(b);

	for (x = 0; aa[x] && bb[x]; x++)
	{
		if (aa[x] !== bb[x])
		{
			var c = Number(aa[x]), d = Number(bb[x]);
			if (c == aa[x] && d == bb[x])
			{
				return c - d;
			}
			else
			{
				return (aa[x] > bb[x]) ? 1 : -1;
			}
		}
	}
	return aa.length - bb.length;
}


/**
 * Returns the outermost parent of a set of elements - assume the following tree
 * where the elements marked with "x" are passed as array to this function:
 *
 * ROOT
 *  |- CHILD 1
 *     |- SUB_CHILD 1
 *     |- SUB_CHILD 2 (x)
 *  |- CHILD 2
 *     |- SUB_CHILD 1
 *         |- SUB_SUB_CHILD 1 (x)
 * The function should now return the "ROOT" elements as this is the outermost
 * parent the elements have in common.
 *
 * TODO: If I think about this, the function actually doesn't work like I'd like
 * 	it to behave...
 */
function egwGridData_getOutermostParent(_elements)
{
	var minElem = null;
	var minCnt = 0;

	for (var i = 0; i < _elements.length; i++)
	{
		var parents = _elements[i].getParentList();

		if (i == 0 || parents.length < minCnt)
		{
			minCnt = parents.length;
			minElem = _elements[i];
		}
	}

	return minElem ? (minElem.parent ? minElem.parent : minElem) : null;
}


/** - egwGridDataReadQueue -- **/

// Some internally used constants
var EGW_DATA_QUEUE_ELEM = 0;
var EGW_DATA_QUEUE_CHILDREN = 1;

// Count of elements which are dynamically added to the update list.
var EGW_DATA_QUEUE_PREFETCH_COUNT = 50;

// Timeout after which the queue events are no longer queued but the actual
// callback function is called.
var EGW_DATA_QUEUE_FLUSH_TIMEOUT = 200;

// Maximum count of elements in the queue after which the queue is flushed
var EGW_DATA_QUEUE_MAX_ELEM_COUNT = 100;

function egwGridDataQueue(_fetchCallback, _context)
{
	this.fetchCallback = _fetchCallback;
	this.context = _context;
	this.dataRoot = null;

	this.queue = [];
	this.queueColumns = [];
	this.timeoutId = 0;

	this.eventQueue = new egwEventQueue();
}

/**
 * Prevents handling the response function - e.g. if the data elements got emptied.
 */
egwGridDataQueue.prototype.flushEventQueue = function()
{
	this.eventQueue.flush();
}

egwGridDataQueue.prototype.setDataRoot = function(_dataRoot)
{
	this.dataRoot = _dataRoot;
}

/**
 * Adds an element to the queue and checks whether its element count is larger
 * than the one specified in EGW_DATA_QUEUE_MAX_ELEM_COUNT. If this is the case,
 * the queue is flushed and false is returned, otherwise true.
 */
egwGridDataQueue.prototype._queue = function(_obj, _last)
{
	this.timeoutId++;

	// Push the queue object onto the queue
	this.queue.push(_obj);

	if (_last && this.queue.length > EGW_DATA_QUEUE_MAX_ELEM_COUNT)
	{
		this.flushQueue(false);
		return false;
	}
	else
	{
		// Specify that the element data is queued
		if (_obj.type != EGW_DATA_QUEUE_CHILDREN)
		{
			for (var i = 0; i < this.queueColumns.length; i++)
			{
				if (typeof _obj.elem.data[this.queueColumns[i]] == "undefined")
				{
					_obj.elem.data[this.queueColumns[i]] = {
						"queued": true
					}
				}
			}
		}

		// Set the flush queue timeout
		if (_last)
		{
			var tid = this.timeoutId;
			var self = this;
			this.eventQueue.queueTimeout(this.flushQueue, this, [true],
				"dataQueueTimeout", EGW_DATA_QUEUE_FLUSH_TIMEOUT);
		}
	}

	return true;
}

egwGridDataQueue.prototype._accumulateQueueColumns = function(_columns)
{
	if (this.dataRoot.columns.columns.length > this.queueColumns.length)
	{
		// Merge the specified columns into the queueColumns variable
		for (var i = 0; i < _columns.length; i++)
		{
			if (this.queueColumns.indexOf(_columns[i]) == -1)
			{
				this.queueColumns.push(_columns[i]);
			}
		}
	}
}

/**
 * Queues the given element in the fetch-data queue.
 *
 * @param object/array _elems is a single element or an array of elements -
 * 	their data will be fetched
 * @param array _columns is an array of column ids which should be fetched. Those
 * 	columns will be accumulated over the queue calls. _columns may also take
 * 	the value EGW_DATA_QUEUE_CHILDREN in which case a request for the children
 * 	of the given element is queued.
 * @param function _callback is a callback function which will be called after
 * 	the data has been sent from the server.
 * @param object _context is the context in which the callback function will
 * 	be executed.
 */
egwGridDataQueue.prototype.queueCall = function(_elems, _columns, _callback, _context)
{
	if (typeof _callback == "undefined")
	{
		_callback = null;
	}
	if (typeof _context == "undefined")
	{
		_context = null;
	}

	if (!(_elems instanceof Array))
	{
		_elems = [_elems];
	}

	for (var i = 0; i < _elems.length; i++)
	{
		var last = i == _elems.length - 1;
		if (_columns === EGW_DATA_QUEUE_CHILDREN)
		{
			this._queue({
					"elem": _elems[i],
					"type": EGW_DATA_QUEUE_CHILDREN,
					"callback": last ? _callback : null,
					"context": _context
				}, last);
		}
		else
		{
			// Accumulate the queue columns ids
			this._accumulateQueueColumns(_columns);

			// Queue the element and search in the elements around the given one for
			// elements whose data isn't loaded yet.
			this._queue({
				"elem": _elems[i],
				"type": EGW_DATA_QUEUE_ELEM,
				"callback": last ? _callback : null,
				"context": _context
			}, last);
		}
	}
}

egwGridDataQueue.prototype._getQueuePlanes = function()
{
	var planes = [];
	var curPlane = null;

	for (var i = 0; i < this.queue.length; i++)
	{
		var elem = this.queue[i].elem;

		if (!curPlane || elem.parent != curPlane.parent)
		{
			curPlane = null;
			for (var j = 0; j < planes.length; j++)
			{
				if (planes[j].parent == elem.parent)
				{
					curPlane = planes[j];
					break;
				}
			}

			if (!curPlane)
			{
				curPlane = {
					"parent": elem.parent,
					"cnt": 0,
					"min": 0,
					"max": 0,
					"idx": 0,
					"done": false
				};
				planes.push(curPlane);
			}
		}

		if (curPlane.cnt == 0 || elem.index < curPlane.min)
		{
			curPlane.min = elem.index;
		}
		if (curPlane.cnt == 0 || elem.index > curPlane.max)
		{
			curPlane.max = elem.index;
		}

		curPlane.cnt++;
	}

	return planes;
}

egwGridDataQueue.prototype.prefetch = function(_cnt)
{
	var cnt = _cnt;
	var planes = this._getQueuePlanes();

	// Set the start indices
	for (var i = 0; i < planes.length; i++)
	{
		planes[i].idx = Math.max(0, Math.ceil(planes[i].min - _cnt / (2 * planes.length)));
	}

	// Add as many elements as specified to the prefetched elements
	var done = 0;
	var plane = 0;
	while (cnt > 0 && done < planes.length)
	{
		if (!planes[plane].done)
		{
			var idx = planes[plane].idx;

			if (!planes[plane].parent || idx == planes[plane].parent.children.length)
			{
				planes[plane].done = true;
				done++;
			}
			else
			{
				var hasData = true;
				var elem = planes[plane].parent.children[idx];
				for (var j = 0; j < this.queueColumns.length; j++)
				{
					if (!elem.hasColumn(this.queueColumns[j], false))
					{
						hasData = false;
						break;
					}
				}

				if (!hasData)
				{
					this._queue({
						"elem": elem,
						"type": EGW_DATA_QUEUE_ELEM,
						"callback": null,
						"context": null
					});
					cnt--;
				}

				planes[plane].idx++;
			}
		}

		// Go to the next plane
		plane = (plane + 1) % planes.length;
	}
}

egwGridDataQueue.prototype.empty = function()
{
	this.queue = [];
}

/**
 * Empties the queue and calls the fetch callback which cares about retrieving
 * the data from the server.
 */
egwGridDataQueue.prototype.flushQueue = function(_doPrefetch)
{
	var ids = [];

	if (_doPrefetch)
	{
		// Get the count of elements which will be dynamically added to the list, "prefetched"
		var prefetch_cnt = Math.min(EGW_DATA_QUEUE_PREFETCH_COUNT,
			Math.max(0, EGW_DATA_QUEUE_MAX_ELEM_COUNT - this.queue.length));

		this.prefetch(prefetch_cnt);
	}

	// Generate a list of element ids
	for (var i = 0; i < this.queue.length; i++)
	{
		var id = this.queue[i].elem.id;
		if (id == this.queue[i].elem.id)
		{
			if (this.queue[i].type == EGW_DATA_QUEUE_CHILDREN)
			{
				id = "[CHILDREN]" + id;
			}
		}

		ids.push(id);
	}

	// Check whether there actually are elements queued...
	if (ids.length > 0)
	{
		// Call the fetch callback and save a snapshot of the current queue
		var queue = this.queue;
		this.fetchCallback.call(this.context, ids, this.queueColumns, function(_data) {
			this.dataCallback(_data, queue);
		}, this);

		this.queue = [];
		this.queueColumns = [];
		this.timeoutId = 0;
	}
}

/**
 * Internal function which is called when the data is received from the fetchCallback.
 *
 * @param _data contains the data which has been retrieved by the fetchCallback
 * @param _queue is the list of elements which had been requested.
 */
egwGridDataQueue.prototype.dataCallback = function(_data, _queue)
{
	var rootData = [];
	try
	{
		// Iterate over the given data and check whether the data coresponds to one
		// of the queue elements - if yes, call their (probably) specified callback.
		// All elements for which no queue element can be found are added to the
		// "rootData" list, which is then loaded by the "dataRoot" data object.
		var i = 0;
		for (var i = 0; i < _data.length; i++)
		{
			var hasTarget = false;

			// Search for a queue element which belongs to the given data entry.
			if (_queue.length > 0 && typeof _data[i].id != "undefined")
			{
				var id = _data[i].id;

				for (var j = 0; j < _queue.length; j++)
				{
					if (_queue[j].elem.id == id)
					{
						_queue[j].elem.loadData(_data[i], true);

						// Call the queue object callback (if specified)
						if (_queue[j].callback)
						{
							_queue[j].callback.call(_queue[j].context);
						}

						// Delete this queue element
						_queue.splice(j, 1);

						hasTarget = true;
						break;
					}
				}
			}

			if (!hasTarget)
			{
				rootData.push(_data[i]);
			}
		}

		this.dataRoot.loadData(rootData, true);
	}
	finally
	{
		this.dataRoot.callEndUpdate();
	}
}

