/**
 * EGroupware eTemplate2 - Class which contains a factory method for rows
 *
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package etemplate
 * @subpackage api
 * @link http://www.egroupware.org
 * @author Andreas Stöckel
 * @copyright Stylite 2012
 * @version $Id$
 */

/*egw:uses
	jquery.jquery;
	et2_core_inheritance;
	et2_core_interfaces;
	et2_core_arrayMgr;
	et2_core_widget;
*/

/**
 * The row provider contains prototypes (full clonable dom-trees) 
 * for all registered row types.
 * 
 * @augments Class
 */
var et2_nextmatch_rowProvider = Class.extend(
{
	/**
	 * Creates the nextmatch row provider.
	 * 
	 * @memberOf et2_nextmatch_rowProvider
	 */
	init: function (_rowProvider, _subgridCallback, _context) {
		// Copy the arguments
		this._rowProvider = _rowProvider;
		this._subgridCallback = _subgridCallback;
		this._context = _context;

		this._createEmptyPrototype();
	},

	/**
	 * Creates the data row prototype.
	 *
	 * @param _widgets is an array containing the root widget for each column.
	 * @param _rowData contains the properties of the root "tr" (like its class)
	 * @param _rootWidget is the parent widget of the data rows (i.e.
	 * the nextmatch)
	 */
	setDataRowTemplate: function(_widgets, _rowData, _rootWidget) {
		// Copy the root widget
		this._rootWidget = _rootWidget;

		// Create the base row
		var row = this._rowProvider.getPrototype("default");

		// Copy the row template
		var rowTemplate = {
			"row": row[0],
			"rowData": _rowData,
			"widgets": _widgets,
			"root": _rootWidget,
			"seperated": null,
			"mgrs": _rootWidget.getArrayMgrs()
		};

		// Create the row widget and insert the given widgets into the row
		var rowWidget = new et2_nextmatch_rowWidget(rowTemplate.mgrs, row[0]);
		rowWidget._parent = _rootWidget;
		rowWidget.createWidgets(_widgets);

		// Get the set containing all variable attributes
		var variableAttributes = this._getVariableAttributeSet(rowWidget);

		// Filter out all widgets which do not implement the et2_IDetachedDOM
		// interface or do not support all attributes listed in the et2_IDetachedDOM
		// interface. A warning is issued for all those widgets as they heavily
		// degrade the performance of the dataview
		var seperated = rowTemplate.seperated = 
			this._seperateWidgets(variableAttributes);

		// Remove all DOM-Nodes of all widgets inside the "remaining" slot from
		// the row-template, then build the access functions for the detachable
		// widgets
		this._stripTemplateRow(rowTemplate);
		this._buildNodeAccessFuncs(rowTemplate);

		// Create the DOM row template
		var tmpl = document.createDocumentFragment();
		row.children().each(function() { tmpl.appendChild(this); });

		this._dataRow = tmpl;
		this._template = rowTemplate;
	},

	getDataRow: function(_data, _row, _idx, _controller) {

		// Clone the row template
		var row = this._dataRow.cloneNode(true);

		// Create array managers with the given data merged in
		var mgrs = et2_arrayMgrs_expand(rowWidget, this._template.mgrs,
			_data, _idx);

		// Insert the widgets into the row which do not provide the functions
		// to set the _data directly
		var rowWidget = null;
		if (this._template.seperated.remaining.length > 0)
		{
			// Transform the variable attributes
			for (var i = 0; i < this._template.seperated.remaining.length; i++)
			{
				var entry = this._template.seperated.remaining[i];

				for (var j = 0; j < entry.data.length; j++)
				{
					var set = entry.data[j];
					entry.widget.options[set.attribute] = mgrs["content"].expandName(set.expression);
				}
			}

			// Create the row widget
			var rowWidget = new et2_nextmatch_rowTemplateWidget(this._rootWidget,
				row);

			// Let the row widget create the widgets
			rowWidget.createWidgets(mgrs, this._template.placeholders);
		}

		// Update the content of all other widgets
		for (var i = 0; i < this._template.seperated.detachable.length; i++)
		{
			var entry = this._template.seperated.detachable[i];

			// Parse the attribute expressions
			var data = {};
			for (var j = 0; j < entry.data.length; j++)
			{
				var set = entry.data[j];
				data[set.attribute] = mgrs["content"].expandName(set.expression);
			}

			// Retrieve all DOM-Nodes
			var nodes = new Array(entry.nodeFuncs.length);
			for (var j = 0; j < nodes.length; j++)
			{
				// Use the previously compiled node function to get the node
				// from the entry
				nodes[j] = entry.nodeFuncs[j](row);
			}

			// Set the array managers first
			entry.widget._mgrs = mgrs;
			if (typeof data.id != "undefined")
			{
				entry.widget.id = data.id;
			}

			// Adjust data for that row
			entry.widget.transformAttributes.call(entry.widget,data);

			// Call the setDetachedAttributes function
			entry.widget.setDetachedAttributes(nodes, data);
		}

		// Insert the row into the tr
		var tr = _row.getDOMNode();
		tr.appendChild(row);

		// Make the row expandable
		if (typeof _data.content["is_parent"] !== "undefined" 
		    && _data.content["is_parent"])
		{
			_row.makeExpandable(true, function () {
				return this._subgridCallback.call(this._context,
						_row, _data, _controller);
			}, this);
		}

		// Set the row data
		this._setRowData(this._template.rowData, tr, mgrs);

		return rowWidget;
	},


	/**
	 * Placeholder for empty row
	 *
	 * The empty row placeholder is used when there are no results to display.
	 * This allows the user to still have a drop target, or use actions that 
	 * do not require a row ID, such as 'Add new'.
	 */
	_createEmptyPrototype: function() {
		var label = this._context && this._context.options && this._context.options.settings.placeholder;
		
		var placeholder = $j(document.createElement("td"))
                                .attr("colspan",this._rowProvider.getColumnCount())
                                .css("height","19px")
                                .text(typeof label != "undefined" && label ? label : egw().lang("No matches found"));
		this._rowProvider._prototypes["empty"] = $j(document.createElement("tr"))
			.addClass("egwGridView_empty")
			.append(placeholder);
	},

	/** -- PRIVATE FUNCTIONS -- **/

	/**
	 * Returns an array containing objects which have variable attributes
	 */
	_getVariableAttributeSet: function(_widget) {
		var variableAttributes = [];

		_widget.iterateOver(function(_widget) {
			// Create the attribtues
			var hasAttr = false;
			var widgetData = {
				"widget": _widget,
				"data": []
			};

			// Get all attribute values
			for (var key in _widget.attributes)
			{
				if (!_widget.attributes[key].ignore &&
				    typeof _widget.options[key] != "undefined")
				{
					var val = _widget.options[key];

					// TODO: Improve detection
					if (typeof val == "string" && val.indexOf("$") >= 0)
					{
						hasAttr = true;
						widgetData.data.push({
							"attribute": key,
							"expression": val
						});
					}
				}
			}

			// Add the entry if there is any data in it
			if (hasAttr)
			{
				variableAttributes.push(widgetData);
			}

		}, this);

		return variableAttributes;
	},

	_seperateWidgets: function(_varAttrs) {
		// The detachable array contains all widgets which implement the
		// et2_IDetachedDOM interface for all needed attributes
		var detachable = [];

		// The remaining array creates all widgets which have to be completely
		// cloned when the widget tree is created
		var remaining = [];

		// Iterate over the widgets
		for (var i = 0; i < _varAttrs.length; i++)
		{
			var widget = _varAttrs[i].widget;

			// Check whether the widget parents are not allready in the "remaining"
			// slot -  if this is the case do not include the widget at all.
			var insertWidget = true;
			var checkWidget = function (_widget) {
				if (_widget.parent != null)
				{
					for (var i = 0; i < remaining.length; i++)
					{
						if (remaining[i].widget == _widget.parent)
						{
							insertWidget = false;
							return;
						}
					}

					checkWidget(_widget.parent);
				}
			};
			checkWidget(widget);

			// Handle the next widget if this one should not be included.
			if (!insertWidget)
			{
				continue;
			}

			// Check whether the widget implements the et2_IDetachedDOM interface
			var isDetachable = false;
			if (widget.implements(et2_IDetachedDOM))
			{
				// Get all attributes the widgets supports to be set in the
				// "detached" mode
				var supportedAttrs = [];
				widget.getDetachedAttributes(supportedAttrs);
				supportedAttrs.push("id");
				isDetachable = true;

				for (var j = 0; j < _varAttrs[i].data.length/* && isDetachable*/; j++)
				{
					var data = _varAttrs[i].data[j];

					var supportsAttr = supportedAttrs.indexOf(data.attribute) != -1;

					if (!supportsAttr)
					{
						egw.debug("warn", "et2_IDetachedDOM widget " +
							widget._type + " does not support " + data.attribute);
					}

					isDetachable &= supportsAttr;
				}
			}

			// Insert the widget into the correct slot
			if (isDetachable)
			{
				detachable.push(_varAttrs[i]);
			}
			else
			{
				remaining.push(_varAttrs[i]);
			}
		}

		return {
			"detachable": detachable,
			"remaining": remaining
		};
	},

	/**
	 * Removes to DOM code for all widgets in the "remaining" slot
	 */
	_stripTemplateRow: function(_rowTemplate) {
		_rowTemplate.placeholders = [];

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

			// Issue a warning - widgets which do not implement et2_IDOMNode
			// are very slow
			egw.debug("warn", "Non-clonable widget '"+ entry.widget._type + "' in dataview row - this " + 
				"might be slow", entry);

			// Set the placeholder for the entry to null
			entry.placeholder = null;

			// Get the outer DOM-Node of the widget
			if (entry.widget.implements(et2_IDOMNode))
			{
				var node = entry.widget.getDOMNode(entry.widget);

				if (node && node.parentNode)
				{
					// Get the parent node and replace the node with a placeholder
					entry.placeholder = document.createElement("span");
					node.parentNode.replaceChild(entry.placeholder, node);
					_rowTemplate.placeholders.push({
						"widget": entry.widget,
						"func": this._compileDOMAccessFunc(_rowTemplate.row,
							entry.placeholder)
					});
				}
			}
		}
	},

	_nodeIndex: function(_node) {
		if(_node.parentNode == null) 
		{
			return 0;
		}
		for (var i = 0; i < _node.parentNode.childNodes.length; i++)
		{
			if (_node.parentNode.childNodes[i] == _node)
			{
				return i;
			}
		}

		return -1;
	},

	/**
	 * Returns a function which does a relative access on the given DOM-Node
	 */
	_compileDOMAccessFunc: function(_root, _target) {
		function recordPath(_root, _target, _path)
		{
			if (typeof _path == "undefined")
			{
				_path = [];
			}

			if (_root != _target && _target)
			{
				// Get the index of _target in its parent node
				var idx = this._nodeIndex(_target);
				if (idx >= 0)
				{
					// Add the access selector
					_path.unshift("childNodes[" + idx + "]");

					// Record the remaining path
					return recordPath.call(this, _root, _target.parentNode, _path);
				}

				throw("Internal error while compiling DOM access function.");
			}
			else
			{
				_path.unshift("_node");
				return "return " + _path.join(".") + ";";
			}
		}

		return new Function("_node", recordPath.call(this, _root, _target));
	},

	/**
	 * Builds relative paths to the DOM-Nodes and compiles fast-access functions
	 */
	_buildNodeAccessFuncs: function(_rowTemplate) {
		for (var i = 0; i < _rowTemplate.seperated.detachable.length; i++)
		{
			var entry = _rowTemplate.seperated.detachable[i];

			// Get all needed nodes from the widget
			var nodes = entry.widget.getDetachedNodes();
			var nodeFuncs = entry.nodeFuncs = new Array(nodes.length);

			// Record the path to each DOM-Node
			for (var j = 0; j < nodes.length; j++)
			{
				nodeFuncs[j] = this._compileDOMAccessFunc(_rowTemplate.row,
					nodes[j]);
			}
		}
	},

	/**
	 * Applies additional row data (like the class) to the tr
	 */
	_setRowData: function (_data, _tr, _mgrs) {
		// TODO: Implement other fields than "class"
		if (_data["class"])
		{
			var classes = _mgrs["content"].expandName(_data["class"]);

			// Get fancy with categories
			var cats = [];
			// Assume any numeric class is a category
			if(_data["class"].indexOf("cat") !== -1 || classes.match(/[0-9]+/))
			{
				// Accept either cat, cat_id or category as ID, and look there for category settings
				var category_location = _data["class"].match(/(cat(_id|egory)?)/);
				if(category_location) category_location = category_location[0];

				// Get actual categories, eg. "cat_15" or "123,456,789", make sure to not match numbers inside other class-names
				cats = classes.match(/(^| |,|cat_)([0-9]+)( |,|$)/g);
				if (!cats) cats = [];
				classes = classes.replace(/(^| |,|cat_)([0-9]+)( |,|$)/g, '');

				// Get category info
				if(!this.categories)
				{
					// Nextmatch category filter should put them here
					var categories = _mgrs["sel_options"].getEntry('cat_id');
					// Or they are in the global - 1 level up
					if(!categories) categories = _mgrs["sel_options"].parentMgr.getEntry('cat_id');
					// If not using category (tracker, calendar list) look for sel_options in the rows
					if(!categories) categories = _mgrs["sel_options"].parentMgr.getEntry(category_location);
					if(!categories) categories = _mgrs["sel_options"].getEntry("${row}["+category_location + "]");
					
					// Cache
					if(categories) this.categories = categories;
				}
				for(var i = 0; i < cats.length; i++)
				{
					// Need cat_, classes can't start with a number
					var cat_id = cats[i];
					cat_id = cat_id.replace(/[^0-9]/g, '');
					var cat_class = 'cat_'+cat_id;

					// Check for existing class
					// TODO

					// Create class
					if(this.categories && this.categories[cat_id] && this.categories[cat_id].color)
					{
						var cat = this.categories[cat_id];
						this._rootWidget.egw().css('.'+cat_class, "background-color: " + cat.color + ";");
						classes += ' '+cat_class;
					}
				}
				classes += " row_category";
			}
			_tr.setAttribute("class", classes);
		}
		if(_data['valign'])
		{
			var align = _mgrs["content"].expandName(_data["valign"]);
			_tr.setAttribute("valign", align);
		}
	}

});

/**
 * @augments et2_widget
 */
var et2_nextmatch_rowWidget = et2_widget.extend(et2_IDOMNode, 
{
	/**
	 * Constructor
	 * 
	 * @param _mgrs
	 * @param _row
	 * @memberOf et2_nextmatch_rowWidget
	 */
	init: function(_mgrs, _row) {
		// Call the parent constructor with some dummy attributes
		this._super(null, {"id": "", "type": "rowWidget"});

		// Initialize some variables
		this._widgets = [];

		// Copy the given DOM node and the content arrays
		this._mgrs = _mgrs;
		this._row = _row;
	},

	/**
	 * Copies the given array manager and clones the given widgets and inserts
	 * them into the row which has been passed in the constructor.
	 */
	createWidgets: function(_widgets) {
		// Clone the given the widgets with this element as parent
		this._widgets = new Array(_widgets.length);
		for (var i = 0; i < _widgets.length; i++)
		{
			this._widgets[i] = _widgets[i].clone(this);
			this._widgets[i].loadingFinished();
			// Set column alignment from widget
			if(this._widgets[i].align)
			{
				this._row.childNodes[i].align = this._widgets[i].align;
			}
		}
	},

	/**
	 * Returns the column node for the given sender
	 */
	getDOMNode: function(_sender) {
		for (var i = 0; i < this._widgets.length; i++)
		{
			if (this._widgets[i] == _sender)
			{
				return this._row.childNodes[i].childNodes[0]; // Return the i-th td tag
			}
		}

		return null;
	}

});

/**
 * @augments et2_widget
 */
var et2_nextmatch_rowTemplateWidget = et2_widget.extend(et2_IDOMNode, 
{
	/**
	 * Constructor
	 * 
	 * @param _root
	 * @param _row
	 * @memberOf et2_nextmatch_rowTemplateWidget
	 */
	init: function(_root, _row) {
		// Call the parent constructor with some dummy attributes
		this._super(null, {"id": "", "type": "rowTemplateWidget"});

		this._root = _root;
		this._mgrs = {};
		this._row = _row;

		// Set parent to root widget, so sub-widget calls still work
		this._parent = _root;

		// Clone the widgets inside the placeholders array
		this._widgets = [];
	},

	createWidgets: function(_mgrs, _widgets) {
		// Set the array managers - don't use setArrayMgrs here as this creates
		// an unnecessary copy of the object
		this._mgrs = _mgrs;

		this._widgets = new Array(_widgets.length);
		for (var i = 0; i < _widgets.length; i++)
		{
			this._row.childNodes[0].childNodes[0];

			this._widgets[i] = {
				"widget": _widgets[i].widget.clone(this),
				"node": _widgets[i].func(this._row)
			};
			this._widgets[i].widget.loadingFinished();
		}
	},

	/**
	 * Returns the column node for the given sender
	 */
	getDOMNode: function(_sender) {

		for (var i = 0; i < this._widgets.length; i++)
		{
			if (this._widgets[i].widget == _sender)
			{
				return this._widgets[i].node;
			}
		}

		return null;
	}

});