/**
 * eGroupWare eTemplate2 - JS Nextmatch object
 *
 * @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 2011
 * @version $Id$
 */

"use strict";

/*egw:uses
	// Force some base libraries to be loaded
	jquery.jquery;
	/phpgwapi/egw_json.js;

	// Include the action system
	egw_action.egw_action;
	egw_action.egw_action_popup;
	egw_action.egw_menu_dhtmlx;

	// Include some core classes
	et2_core_widget;
	et2_core_interfaces;
	et2_core_DOMWidget;

	// Include all widgets the nextmatch extension will create
	et2_widget_template;
	et2_widget_grid;
	et2_widget_selectbox;

	// Include the dynheight manager
	et2_extension_nextmatch_dynheight;

	// Include the grid classes
	et2_dataview_view_gridContainer;
	et2_dataview_model_dataProvider;
	et2_dataview_model_columns;

*/

/**
 * Interface all special nextmatch header elements have to implement.
 */
var et2_INextmatchHeader = new Interface({

	/**
	 * The 'setNextmatch' function is called by the parent nextmatch widget
	 * and tells the nextmatch header widgets which widget they should direct
	 * their 'sort', 'search' or 'filter' calls to.
	 */
	setNextmatch: function(_nextmatch) {}
});

var et2_INextmatchSortable = new Interface({

	setSortmode: function(_mode) {}

});

/**
 * Class which implements the "nextmatch" XET-Tag
 */ 
var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, {

	attributes: {
		"template": {
			"name": "Template",
			"type": "string",
			"description": "The id of the template which contains the grid layout."
		},
		"settings": {
			"name": "Settings",
			"type": "any",
			"description": "The nextmatch settings"
		}
	},

	legacyOptions: ["template"],
	createNamespace: true,

	init: function() {
		this._super.apply(this, arguments);

		this.div = $j(document.createElement("div"))
			.addClass("et2_nextmatch");

		this.header = new et2_nextmatch_header_bar(this, this.div);

		// Create the dynheight component which dynamically scales the inner
		// container.
		this.dynheight = new et2_dynheight(null, this.div, 150);

		// Create the action manager
		this.actionManager = new egwActionManager();

		// Create the data provider which cares about streaming the row data
		// efficiently to the rows.
		var total = typeof this.options.settings.total != "undefined" ?
			this.options.settings.total : 0;
		this.dataProvider = new et2_dataview_dataProvider(this, total,
			this.actionManager);

		// Load the first data into the dataProvider
		if (this.options.settings.rows)
		{
			this.dataProvider.loadData({"rows": this.options.settings.rows});
		}

		// Create the outer grid container
		this.dataviewContainer = new et2_dataview_gridContainer(this.div,
			this.dataProvider);

		this.activeFilters = {};
	},

	/**
	 * Destroys all 
	 */
	destroy: function() {
		// Free the grid components
		this.dataviewContainer.free();
		this.dataProvider.free();
		this.dynheight.free();

		this._super.apply(this, arguments);
	},

	/**
	 * Loads the nextmatch settings
	 */
	transformAttributes: function(_attrs) {
		this._super.apply(this, arguments);

		if (this.id)
		{
			var entry = this.getArrayMgr("content").data;

			if (entry)
			{
				_attrs["settings"] = entry;
			}
		}
	},

	/**
	 * Implements the et2_IResizeable interface - lets the dynheight manager
	 * update the width and height and then update the dataview container.
	 */
	resize: function() {
		this.dynheight.update(function(_w, _h) {
			this.dataviewContainer.resize(_w, _h);
		}, this);
	},

	/**
	 * Get Rows callback
	 */
	getRows: function(_fetchList, _callback, _context) {
		// Create an ajax-request
		var request = new egw_json_request(
			"etemplate_widget_nextmatch::ajax_get_rows::etemplate", [
					this.getInstanceManager().etemplate_exec_id,
					_fetchList,
					this.activeFilters
				], this);

		var nextmatch = this;
		// Send the request
		request.sendRequest(true, function(_data) {
			_callback.call(_context, _data);
			// Let anything else know
			nextmatch.div.trigger({type:'nm_data', nm_data: _data, 'nextmatch': nextmatch});
		}, null);
	},

	/**
	 * Sorts the nextmatch widget by the given ID.
	 *
	 * @param _id is the id of the data entry which should be sorted.
	 * @param _asc if true, the elements are sorted ascending, otherwise
	 * 	descending. If not set, the sort direction will be determined
	 * 	automatically.
	 */
	sortBy: function(_id, _asc, _update) {
		if (typeof _update == "undefined")
		{
			_update = true;
		}

		// Create the "sort" entry in the active filters if it did not exist
		// yet.
		if (typeof this.activeFilters["sort"] == "undefined")
		{
			this.activeFilters["sort"] = {
				"id": null,
				"asc": true
			};
		}

		// Determine the sort direction automatically if it is not set
		if (typeof _asc == "undefined")
		{
			if (this.activeFilters["sort"].id == _id)
			{
				_asc = !this.activeFilters["sort"].asc;
			}
		}

		// Update the entry in the activeFilters object
		this.activeFilters["sort"] = {
			"id": _id,
			"asc": _asc
		}

		// Set the sortmode display
		this.iterateOver(function(_widget) {
			_widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none");
		}, this, et2_INextmatchSortable);

		if (_update)
		{
			this.applyFilters();
		}
	},

	/**
	 * Removes the sort entry from the active filters object and thus returns to
	 * the natural sort order.
	 */
	resetSort: function() {
		// Check whether the nextmatch widget is currently sorted
		if (typeof this.activeFilters["sort"] != "undefined")
		{
			// Reset the sortmode
			this.iterateOver(function(_widget) {
				_widget.setSortmode("none");
			}, this, et2_INextmatchSortable);

			// Delete the "sort" filter entry
			delete(this.activeFilters["sort"]);
			this.applyFilters();
		}
	},

	applyFilters: function() {
		et2_debug("info", "Changing nextmatch filters to ", this.activeFilters);

		// Clear the dataprovider and the dataview container - this will cause
		// the grid to reload.
		this.dataProvider.clear();
		this.dataviewContainer.clear();
	},

	/**
	 * Generates the column caption for the given column widget
	 */
	_genColumnCaption: function(_widget) {
		var result = null;

		if(typeof _widget._genColumnCaption == "function") return _widget._genColumnCaption();

		_widget.iterateOver(function(_widget) {
			if (!result)
			{
				result = _widget.options.label;
			}
			else
			{
				result += ", " + _widget.options.label;
			}
		}, this, et2_INextmatchHeader);

		return result;
	},

	/**
	 * Generates the column name (internal) for the given column widget
	 * Used in preferences to refer to the columns by name instead of position
	 *
	 * See _getColumnCaption() for human fiendly captions
	 */
	_getColumnName: function(_widget) {
		if(typeof _widget._getColumnName == 'function') return _widget._getColumnName();

		var name = _widget.id;
		var child_names = [];
		var children = _widget.getChildren();
		for(var i = 0; i < children.length; i++) {
			if(children[i].id) child_names.push(children[i].id);
		}

		var colName =  name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_");
		if(colName == "") {
			et2_debug("info", "Unable to generate nm column name for ", _widget);
		}
		return colName;
	},


	/**
	 * Apply stored user preferences to discovered columns
	 */
	_applyUserPreferences: function(_row, _colData) {
		// Read preference or default for column visibility
		var negated = false;
		var columnPreference = "";
		if(this.options.settings.default_cols)
		{
			negated = this.options.settings.default_cols[0] == "!";
			columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols;
		}
		if(this.options.settings.selectcols)
		{
			columnPreference = this.options.settings.selectcols;
		}
		if(!this.options.settings.columnselection_pref)
		{
			// Set preference name so changes are saved
			this.options.settings.columnselection_pref = this.options.template;
		}
		if(this.options.settings.columnselection_pref) {
			var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, ".");
			var app = list[0];
			// 'nextmatch-' prefix is there in preference name, but not in setting, so add it in
			var pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, list[0]);
			if(pref) 
			{
				negated = (pref[0] == "!");
				columnPreference = negated ? pref.substring(1) : pref;
			}
		}
		var columnDisplay = et2_csvSplit(columnPreference,null,",");

		// Adjusted column sizes
		var size = {};
		if(this.options.settings.columnselection_pref && app)
		{
			size = egw.preference("nextmatch-"+this.options.settings.columnselection_pref+"-size", app);
		}
		if(!size) size = {};

		// Add in display preferences
		if(columnDisplay && columnDisplay.length > 0)
		{
			RowLoop:
			for(var i = 0; i < _row.length; i++)
			{
				var colName = this._getColumnName(_row[i].widget);
				if(!colName) continue;

				if(size[colName]) _colData[i].width = size[colName];
				// Customfields needs special processing
				if(_row[i].widget.instanceOf(et2_nextmatch_customfields))
				{
					// Find cf field
					for(var j = 0; j < columnDisplay.length; j++)
					{
						if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) {
							var cfDisplay = et2_csvSplit(columnDisplay[j],null,"_#");
							_row[i].widget.options.fields = {};
							for(var k = 1; k < cfDisplay.length; k++)
							{
								_row[i].widget.options.fields[cfDisplay[k]] = true;
							} 
							// Resets field visibility too
							_row[i].widget._getColumnName();
							_colData[i].disabled = negated;
							continue RowLoop;
						}
					}
				}
				for(var j = 0; j < columnDisplay.length; j++)
				{
					if(columnDisplay[j] == colName)
					{
						_colData[i].disabled = negated;
						continue RowLoop;
					}
				}
				_colData[i].disabled = !negated;
			}
		}
	},

	/**
	 * Take current column display settings and store them in egw.preferences
	 * for next time
	 */
	_updateUserPreferences: function() {
		var colMgr = this.dataviewContainer.getColumnMgr()
		if(!this.options.settings.columnselection_pref) {
			this.options.settings.columnselection_pref = this.options.template;
		}

		var visibility = colMgr.getColumnVisibilitySet();
		var colDisplay = [];
		var colSize = {};
		var custom_fields = [];

		// visibility is indexed by internal ID, widget is referenced by position, preference needs name
		for(var i = 0; i < colMgr.columns.length; i++)
		{
			var widget = this.columns[i].widget;
			var colName = this._getColumnName(widget);
			if(colName) {
				if(visibility[colMgr.columns[i].id].visible) colDisplay.push(colName);
				if(colMgr.columns[i].fixedWidth) colSize[colName] = colMgr.columns[i].fixedWidth;
				// Server side wants each cf listed as a seperate column
				if(widget.instanceOf(et2_nextmatch_customfields)) 
				{
					for(name in widget.options.fields) {
						 if(widget.options.fields[name]) custom_fields.push("#"+name);
					}
				}
			} else if (colMgr.columns[i].fixedWidth) {
				et2_debug("info", "Could not save column width - no name", colMgr.columns[i].id);
			}
		}
			
		var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, ".");
		var app = list[0];

		// Save visible columns
		// 'nextmatch-' prefix is there in preference name, but not in setting, so add it in
		egw.set_preference(app, "nextmatch-"+this.options.settings.columnselection_pref, colDisplay.join(","));

		// Save adjusted column sizes
		egw.set_preference(app, "nextmatch-"+this.options.settings.columnselection_pref+"-size", colSize);

		// Update query value, so data source can use visible columns to exclude expensive sub-queries
		var oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : [];

		// Server side wants each cf listed as a seperate column
		jQuery.merge(colDisplay, custom_fields);

		this.activeFilters.selectcols = colDisplay;

		// We don't need to re-query if they've removed a column
		var changed = [];
		ColLoop:
		for(var i = 0; i < colDisplay.length; i++)
		{
			for(var j = 0; j < oldCols.length; j++) {
				 if(colDisplay[i] == oldCols[j]) continue ColLoop;
			}
			changed.push(colDisplay[i]);
		}
		if(changed.length)
		{
			this.applyFilters();
		}
	},

	_parseHeaderRow: function(_row, _colData) {
		// Get column display preference
		this._applyUserPreferences(_row, _colData);

		// Go over the header row and create the column entries
		this.columns = new Array(_row.length);
		var columnData = new Array(_row.length);
		for (var x = 0; x < _row.length; x++)
		{
			this.columns[x] = {
				"widget": _row[x].widget
			};


			columnData[x] = {
				"id": "col_" + x,
				"caption": this._genColumnCaption(_row[x].widget),
				"visibility": _colData[x].disabled ?
					ET2_COL_VISIBILITY_INVISIBLE : ET2_COL_VISIBILITY_VISIBLE,
				"width": _colData[x].width
			};

			// Append the widget to this container
			this.addChild(_row[x].widget);
		}

		// Create the column manager and update the grid container
		this.dataviewContainer.setColumns(columnData);

		var self = this;
		// Register handler to update preferences when column properties are changed
		this.dataviewContainer.onUpdateColumns = function() { self._updateUserPreferences();};
		
		// Register handler for column selection popup
		this.dataviewContainer.selectColumnsClick = function(event) { self._selectColumnsClick(event);};
	},

	_parseDataRow: function(_row, _colData) {
		var columnWidgets = new Array(this.columns.length);

		for (var x = 0; x < columnWidgets.length; x++)
		{
			if (typeof _row[x] != "undefined" && _row[x].widget)
			{
				columnWidgets[x] = _row[x].widget;

				// Append the widget to this container
				this.addChild(_row[x].widget);
			}
			else
			{
				columnWidgets[x] = _row[x].widget;
			}
		}

		this.dataviewContainer.rowProvider.setDataRowTemplate(columnWidgets, this);
	},

	_parseGrid: function(_grid) {
		// Search the rows for a header-row - if one is found, parse it
		for (var y = 0; y < _grid.rowData.length; y++)
		{
			// Parse the first row as a header, need header to parse the data rows 
			if (_grid.rowData[y]["class"] == "th" || y == 0)
			{
				this._parseHeaderRow(_grid.cells[y], _grid.colData);
			}
			else
			{
				this._parseDataRow(_grid.cells[y], _grid.colData);
			}
		}
	},

	_selectColumnsClick: function(e) {
		var self = this;
		var columnMgr = this.dataviewContainer.columnMgr;
		var columns = {};
		var columns_selected = [];
		for (var i = 0; i < columnMgr.columns.length; i++)
		{
			var col = columnMgr.columns[i];
			var widget = this.columns[i].widget;

			if(col.caption && col.visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT)
			{
				columns[col.id] = col.caption;
				if(col.visibility == ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(col.id);
			}
			// Custom fields get listed separately
			if(widget.instanceOf(et2_nextmatch_customfields))
			{
				for(var field_name in widget.customfields)
				{
					columns[et2_customfields_list.prototype.prefix+field_name] = " - "+widget.customfields[field_name].label;
					if(widget.fields[field_name]) columns_selected.push(et2_customfields_list.prototype.prefix+field_name);
				}
			}
		}

		// Build the popup
		if(!this.selectPopup)
		{
			var select = et2_createWidget("select", {multiple: true, rows: 8}, this);
			select.set_select_options(columns);
			select.set_value(columns_selected);

			var defaultCheck = et2_createWidget("checkbox", {}, this);
			defaultCheck.set_id('as_default');
			defaultCheck.set_label(egw.lang("As default"));

			var okButton = et2_createWidget("buttononly", {}, this);
			okButton.set_label(egw.lang("ok"));
			okButton.onclick = function() {
				// Update visibility
				var visibility = {};
				for (var i = 0; i < columnMgr.columns.length; i++)
				{
					var col = columnMgr.columns[i];
					if(col.caption && col.visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT )
					{
						visibility[col.id] = {visible: false};
					}
				}
				var value = select.getValue();
				var column = 0;
				for(var i = 0; i < value.length; i++)
				{
					if(visibility[value[i]])
					{
						visibility[value[i]].visible = true;
					}
					// Custom fields are listed seperately in column list, but are only 1 column
					if(self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) {
						var cf = self.columns[column].widget.options.customfields;
						var visible = self.columns[column].widget.options.fields;

						// Turn off all custom fields
						for(var field_name in cf)
						{
							visible[field_name] = false;
						}
						i++;
						// Turn on selected custom fields - start from 0 in case they're not in order
						for(var j = 0; j < value.length; j++)
						{
							if(value[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue;
							visible[value[j].substring(1)] = true;
							i++;
						}
					}
					column++;
				}
				columnMgr.setColumnVisibilitySet(visibility);
				self.selectPopup.toggle();

				self.dataviewContainer.updateColumns();

				// Set default?
				if(defaultCheck.get_value() == "true")
				{
					self.getInstanceManager().submit();
				}
			};

			var cancelButton = et2_createWidget("buttononly", {}, this);
			cancelButton.set_label(egw.lang("cancel"));
			cancelButton.onclick = function() {
				self.selectPopup.toggle();
			}

			this.selectPopup = jQuery(document.createElement("fieldset"))
				.addClass("colselection ui-dialog")
				.append("<legend>"+egw.lang("Select columns")+"</legend>")
				.append(select.getDOMNode())
				.append(okButton.getDOMNode())
				.append(cancelButton.getDOMNode())
				.appendTo(this.div);

			// Add default checkbox for admins
			var apps = egw.user('apps');
			if(apps['admin'])
			{
				this.selectPopup.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode()))
			}
		}
		else	
		{
			this.selectPopup.toggle();
		}
		var t_position = jQuery(e.target).position();
		var s_position = this.div.position();
		this.selectPopup.css("top", t_position.top)
			.css("left", s_position.left + this.div.width() - this.selectPopup.width());
	},

	/**
	 * When the template attribute is set, the nextmatch widget tries to load
	 * that template and to fetch the grid which is inside of it. It then calls
	 */
	set_template: function(_value) {
		if (!this.template)
		{
			// Load the template
			var template = et2_createWidget("template", {"id": _value}, this);

			if (!template.proxiedTemplate)
			{
				et2_debug("error", "Error while loading definition template for" + 
					"nextmatch widget.");
				return;
			}

			// Fetch the grid element and parse it
			var definitionGrid = template.proxiedTemplate.getChildren()[0];
			if (definitionGrid && definitionGrid instanceof et2_grid)
			{
				this._parseGrid(definitionGrid);
			}
			else
			{
				et2_debug("error", "Nextmatch widget expects a grid to be the " + 
					"first child of the defined template.");
				return;
			}

			// Free the template again
			template.free();

			// Call the "setNextmatch" function of all registered
			// INextmatchHeader widgets.
			this.iterateOver(function (_node) {
				_node.setNextmatch(this);
			}, this, et2_INextmatchHeader);

			// Load the default sort order
			if (this.options.settings.order && this.options.settings.sort)
			{
				this.sortBy(this.options.settings.order,
					this.options.settings.sort == "ASC", false);
			}
		}
	},

	/**
	 * Activates the actions
	 */
	set_settings: function(_settings) {
		if (_settings.actions)
		{
			// Read the actions from the settings array
			this.actionManager.updateActions(_settings.actions);
			this.actionManager.setDefaultExecute("javaScript:nm_action");
			// this is rather hackisch, but I have no idea how to get the action_link & row_id to the actionObject of the row otherwise
			this.actionManager.action_links = _settings.action_links;
			this.actionManager.row_id = _settings.row_id;
		}
	},

	getDOMNode: function(_sender) {
		if (_sender == this)
		{
			return this.div[0];
		}

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

		// Let header have a chance
		if(_sender._parent && _sender._parent == this)
		{
			return this.header.getDOMNode(_sender);
		}

		return null;
	},

	getPath: function() {
		var path = this._super.apply(this,arguments);
		if(this.id) path.push(this.id);
		return path;
	}

});

et2_register_widget(et2_nextmatch, ["nextmatch"]);

/**
 * Standard nextmatch header bar, containing filters, search, record count, letter filters, etc.
 *
 * Unable to use an existing template for this because parent (nm) doesn't, and template widget doesn't
 * actually load templates from the server.
 */
var et2_nextmatch_header_bar = Class.extend(et2_INextmatchHeader, {
	attributes: {
		"filter_label": {
			"name": "Filter label",
			"type": "string",
			"description": "Label for filter",
			"default": "",
			"translate": true
		},
		"filter_help": {
			"name": "Filter help",
			"type": "string",
			"description": "Help message for filter",
			"default": "",
			"translate": true
		},
		"filter": {
			"name": "Filter value",
			"type": "any",
			"description": "Current value for filter",
			"default": ""
		},
		"no_filter": {
			"name": "No filter",
			"type": "boolean",
			"description": "Remove filter",
			"default": false
		}
	},

	init: function(nextmatch, nm_div) {
		this.nextmatch = nextmatch;
		
		this.div = jQuery(document.createElement("div"))
			.addClass("nextmatch_header");
		if(this.nextmatch) this._createHeader();
	},

	destroy: function() {
		this.nextmatch = null;
		this.div = null;
	},

	setNextmatch: function(nextmatch) {
		if(this.div) this.div.remove();
		this.nextmatch = nextmatch;
		this._createHeader();
	},

	_createHeader: function() {

		var self = this;
		var nm_div = this.nextmatch.div;
		var settings = this.nextmatch.options.settings;

		this.div.prependTo(nm_div);

		// Left

		// Record count
		this.count = jQuery(document.createElement("span"))
			.addClass("header_count ui-corner-all");

		// Need to figure out how to update this as grid scrolls
		//this.count.append("? - ? ").append(egw.lang("of")).append(" ");
		this.count_total = jQuery(document.createElement("span"))
			.appendTo(this.count)
			.text(settings.total + "");

		// Set up so if row count changes, display is updated
		nm_div.bind('nm_data', function(e) { // Have to bind to DOM node, not et2 widget
			self.count_total.text(e.nm_data.total);
		});

		// Right

		this.filters = jQuery(document.createElement("div")).appendTo(this.div)
			.addClass("filters");
		

		// Add category
		if(!settings.no_cat) {
			settings.cat_id_label = egw.lang("Category");
			this.category = this._build_select('cat_id', 'select-cat', settings.cat_id, true);
		}

		// Filter 1
		if(!settings.no_filter) {
			this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang);
		}

		// Filter 2
		if(!settings.no_filter2) {
			this.filter2 = this._build_select('filter2', 'select', settings.filter2, settings.filter2_no_lang);
		}


		// Export
		if(!settings.no_csv_export)
		{
			var definition = settings.csv_fields;
			if(settings.csv_fields === true)
			{
				definition = egw.preference('nextmatch-export-definition', this.nextmatch.getTemplateApp());
			}
			var button = et2_createWidget("buttononly", {"label": "Export", image:"phpgwapi/filesave"}, this.nextmatch);
			jQuery(button.getDOMNode()).appendTo(this.filters).css("float", "right")
				.click(this.nextmatch, function(event) {
					egw_openWindowCentered2( egw.link('/index.php', {
						'menuaction':	'importexport.importexport_export_ui.export_dialog',
						'appname':	event.data.getTemplateApp(),
						'definition':	definition
					}), '_blank', 850, 440, 'yes');
				});
		}

		this.count.appendTo(this.filters);

		// Search
		this.search = et2_createWidget("textbox", {"blur":egw.lang("search")}, this.nextmatch);
		this.search.input.attr("type", "search");
		this.search.input.val(settings.search);
		
		jQuery(document.createElement("button"))
			.appendTo(this.filters)
			.text(">")
			.click(this.nextmatch, function(event) {
				event.data.activeFilters.search = self.search.getValue()
				event.data.applyFilters();
			});

		

		// Letter search
		var current_letter = this.nextmatch.options.settings.searchletter ? 
			this.nextmatch.options.settings.searchletter : 
			(this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false);
		if(this.nextmatch.options.settings.lettersearch || current_letter)
		{
			this.lettersearch = jQuery(document.createElement("table"))
				.css("width", "100%")
				.appendTo(this.div);
			var tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch);
			var row = jQuery(document.createElement("tr")).appendTo(tbody);

			for(var i = 65; i <= 90; i++) {
				var button = jQuery(document.createElement("td"))
					.addClass("lettersearch")
					.appendTo(row)
					.attr("id", String.fromCharCode(i))
					.text(String.fromCharCode(i));
				if(String.fromCharCode(i) == current_letter) button.addClass("lettersearch_active");
			}
			button = jQuery(document.createElement("td"))
				.addClass("lettersearch")
				.appendTo(row)
				.attr("id", "")
				.text(egw.lang("all"));
			if(!current_letter) button.addClass("lettersearch_active");

			this.lettersearch.click(this.nextmatch, function(event) {
				// this is the lettersearch table
				jQuery("td",this).removeClass("lettersearch_active");
				jQuery(event.target).addClass("lettersearch_active");
				var letter = event.target.id;
				event.data.activeFilters.searchletter = (letter == "" ? false : letter);
				event.data.applyFilters();
			});
		}
	},


	/**
	 * Build the selectbox filters in the header bar
	 * Sets value, options, labels, and change handlers
	 */
	_build_select: function(name, type, value, lang) {
		// Create widget
		var select = et2_createWidget(type, {
			"id": this.nextmatch.id + "_"+name,
			"label": this.nextmatch.options.settings[name+"_label"]
		},this.nextmatch);

		// Set options
		// Check in content for options-<name>
		var mgr = this.nextmatch.getArrayMgr("content");
		var options = mgr.getEntry("options-" + name);
		// Look in sel_options
		if(!options) options = this.nextmatch.getArrayMgr("sel_options").getEntry(name);
		// Check parent sel_options, because those are usually global and don't get passed down
		if(!options) options = this.nextmatch.getArrayMgr("sel_options").parentMgr.getEntry(name);
		if(options) select.set_select_options(options);

		// Set value
		select.set_value(value);

		// Set onChange
		var input = select.input;
		if(this.nextmatch.options.settings[name+"_onchange"])
		{
			var onchange = this.nextmatch.options.settings[name+"_onchange"];
			// onchange needs to get current values
			if(typeof onchange == "string") {
				// Don't change original so we can do this again
				onchange = et2_js_pseudo_funcs(onchange, this.nextmatch.id);
				if(onchange.indexOf("$") >= 0 || onchange.indexOf("@") >= 0) {
					var mgr = this.nextmatch.getArrayMgr("content");
					if(mgr) onchange = mgr.expandName(onchange);
				}
				onchange = new Function(onchange);
			}
			input.change(this.nextmatch, function(event) {
				onchange(event);
			});
		}
		else
		{
			input.change(this.nextmatch, function(event) {
				event.data.activeFilters[name] = input.val()
				event.data.applyFilters();
			});
		}
			
		return select;
	},

	/**
	 * Help out nextmatch / widget stuff by checking to see if sender is part of header
	 */
	getDOMNode: function(_sender) {
		var filters = [this.category, this.filter, this.filter2, this.search];
		for(var i = 0; i < filters.length; i++)
		{
			if(_sender == filters[i]) 
			{
				return this.filters[0];
			}
		}
		return null;
	}

});
et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]);

/**
 * Classes for the nextmatch sortheaders etc.
 */
var et2_nextmatch_header = et2_baseWidget.extend(et2_INextmatchHeader, {

	attributes: {
		"label": {
			"name": "Caption",
			"type": "string",
			"description": "Caption for the nextmatch header",
			"translate": true
		}
	},

	init: function() {
		this._super.apply(this, arguments);

		this.labelNode = $j(document.createElement("span"));
		this.nextmatch = null;

		this.setDOMNode(this.labelNode[0]);
	},

	destroy: function() {
		this._super.apply(this, arguments);
	},

	/**
	 * Set nextmatch is the function which has to be implemented for the
	 * et2_INextmatchHeader interface.
	 */
	setNextmatch: function(_nextmatch) {
		this.nextmatch = _nextmatch;
	},

	set_label: function(_value) {
		this.label = _value;

		this.labelNode.text(_value);
	}

});

et2_register_widget(et2_nextmatch_header, ['nextmatch-header',
	'nextmatch-customfilter']);

/**
 * Extend header to process customfields
 */
var et2_nextmatch_customfields = et2_nextmatch_header.extend({
	attributes: {
		'customfields': {
                        'name': 'Custom fields',
                        'description': 'Auto filled'
                },
		'fields': {
			'name': "Visible fields",
			"description": "Auto filled"
		}
	},

	init: function() {

		// Create the table body and the table
                this.tbody = $j(document.createElement("tbody"));
                this.table = $j(document.createElement("table"))
                        .addClass("et2_grid");
                this.table.append(this.tbody);
		this.rows = {};

		this._super.apply(this, arguments);
	},

	destroy: function() {
		this.rows = null;
		this.tbody = null;
		this.table = null;
	},

	transformAttributes: function(_attrs) {
		this._super.apply(this, arguments);

		// Add in settings that are objects
		if(!_attrs.customfields)
		{
			// Check for custom stuff (unlikely)
			var data = this.getArrayMgr("modifications").getEntry(this.id);
			// Check for global settings
			if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true);
			for(var key in data)
			{
				if(data[key] instanceof Object && ! _attrs[key]) _attrs[key] = data[key];
			}
		}
	},

	setNextmatch: function(_nextmatch) {
		this._super.apply(this, arguments);
		this.loadFields();
	},

	/**
	 * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio
	 */
	loadFields: function() {
		var columnMgr = this.nextmatch.dataviewContainer.columnMgr;
		var nm_column = null;
		for(var i = 0; i < this.nextmatch.columns.length; i++)
		{
			if(this.nextmatch.columns[i].widget == this)
			{
				nm_column = columnMgr.columns[i];
				break;
			}
		}
		if(!nm_column) return;
		var apps = egw.link_app_list();
		for(var field_name in this.options.customfields)
		{
			var field = this.options.customfields[field_name];

			
			if(this.rows[field_name]) continue;

			// Table row
			var row = jQuery(document.createElement("tr"))
                                .appendTo(this.tbody);
                        var cf = jQuery(document.createElement("td"))
                                .appendTo(row);
			this.rows[field_name] = cf[0];

			// Create widget by type
			var widget = null;
			var cf_id = et2_customfields_list.prototype.prefix + field_name;

			if(field.type == 'select')
			{
				widget = et2_createWidget("nextmatch-filterheader", {
					id: cf_id,
					label: field.label,
					select_options: field.values
				}, this);
			}
			else if (apps[field.type]) 
			{
				// TODO: Figure out why search can't get focus
				widget = et2_createWidget("nextmatch-entryheader", {
					id: cf_id,
					application: field.type,
					blur: field.label
				}, this);
			}
			else
			{
				widget = et2_createWidget("nextmatch-sortheader", {
					id: cf_id,
					label: field.label
				}, this);
			}
			// Not sure why this is needed, widget should be added by et2_createWidget()
			if(widget) cf.append(widget.getDOMNode());

			// Check for column filter
			if(!jQuery.isEmptyObject(this.options.fields) && (
				this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined'))
			{
				cf.hide();
			}
		}
	},

	getDOMNode: function(_sender) {
		// If the parent class functions are asking for the DOM-Node, return the
		// outer table.
		if (_sender == this)
		{
			return this.table[0];
		}

		// Check whether the _sender object exists inside the management array
		if(this.rows && _sender.id && this.rows[_sender.id])
		{
			// Empty it, to avoid doubled DOM nodes
			//jQuery(this.rows[_sender.id]).empty();
			return this.rows[_sender.id];
		}

		return null;
	},

	/**
	 * Provide own column caption (column selection)
	 *
	 * If only one custom field, just use that, otherwise use "custom fields"
	 */
	_genColumnCaption: function() {
		return egw.lang("Custom fields");
	},

	/**
	 * Provide own column naming, including only selected columns
	 */
	_getColumnName: function() {
		var name = this.id;
		var visible = [];
		for(var field_name in this.options.customfields)
		{
			if(jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true)
			{
				visible.push(et2_customfields_list.prototype.prefix + field_name);
				jQuery(this.rows[field_name]).show();
			}
			else if (typeof this.rows[field_name] != "undefined")
			{
				jQuery(this.rows[field_name]).hide();
			}
		}

		

		if(visible.length) {
			name  +="_"+ visible.join("_");
		}
		else
		{
			// None hidden means all visible
			jQuery(this.rows[field_name]).parent().parent().children().show();
		}

		// Update global custom fields column(s) - widgets will check on their own

		// Check for custom stuff (unlikely)
		var data = this.getArrayMgr("modifications").getEntry(this.id);
		// Check for global settings
		if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true);
		if(!data.fields) data.fields = {};
		for(var field in this.options.customfields)
		{
			data.fields[field] = (typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]);
		}
		return name;
	}
});
et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']);

var et2_nextmatch_sortheader = et2_nextmatch_header.extend(et2_INextmatchSortable, {

	init: function() {
		this._super.apply(this, arguments);

		this.sortmode = "none";

		this.labelNode.addClass("nextmatch_sortheader none");
	},

	click: function() {
		if (this.nextmatch && this._super.apply(this, arguments))
		{
			this.nextmatch.sortBy(this.id);
			return true;
		}

		return false;
	},

	/**
	 * Function which implements the et2_INextmatchSortable function.
	 */
	setSortmode: function(_mode) {
		// Remove the last sortmode class and add the new one
		this.labelNode.removeClass(this.sortmode)
			.addClass(_mode);

		this.sortmode = _mode;
	}

});

et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']);


var et2_nextmatch_filterheader = et2_selectbox.extend(et2_INextmatchHeader, {

	/**
	 * Override to add change handler
	 */
	createInputWidget: function() {
		// Make sure there's an option for all
		if(!this.options.empty_label && !this.options.select_options[""])
		{
			this.options.empty_label = this.options.label ? this.options.label : egw.lang("All");
		}
		this._super.apply(this, arguments);

		this.input.change(this, function(event) {
			if(typeof event.data.nextmatch.activeFilters.col_filter == 'undefined')
				event.data.nextmatch.activeFilters.col_filter = {};
			if(event.data.input.val())
			{
				event.data.nextmatch.activeFilters["col_filter"][event.data.id] = event.data.input.val()
			}
			else
			{
				delete (event.data.nextmatch.activeFilters["col_filter"][event.data.id]);
			}
			event.data.nextmatch.applyFilters();
		});
	},

	/**
	 * Set nextmatch is the function which has to be implemented for the
	 * et2_INextmatchHeader interface.
	 */
	setNextmatch: function(_nextmatch) {
		this.nextmatch = _nextmatch;

		// Set current filter value from nextmatch settings
		if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id])
		{
			this.set_value(this.nextmatch.options.settings.col_filter[this.id]);
		}
	}

});

et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader',
	'nextmatch-accountfilter']);

var et2_nextmatch_entryheader = et2_link_entry.extend(et2_INextmatchHeader, {

	/**
	 * Override to add change handler
	 */
	select: function(event, selected) {
		this._super.apply(this, arguments);
		if(typeof this.nextmatch.activeFilters.col_filter == 'undefined')
			this.nextmatch.activeFilters.col_filter = {};
		if(selected && selected.item.value) {
			this.nextmatch.activeFilters["col_filter"][this.id] = selected.item.value;
		} else {
			delete (this.nextmatch.activeFilters["col_filter"][this.id]);
		}
		this.nextmatch.applyFilters();
	},

	/**
	 * Set nextmatch is the function which has to be implemented for the
	 * et2_INextmatchHeader interface.
	 */
	setNextmatch: function(_nextmatch) {
		this.nextmatch = _nextmatch;

		// Set current filter value from nextmatch settings
		if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id])
		{
			this.set_value(this.nextmatch.options.settings.col_filter[this.id]);
		}
		var self = this;
		// Fire on lost focus, clear filter if user emptied box
		this.search.focusout(this, function(event) {if(!self.search.val()) { self.select(event, {item:{value:null}});}});
	}
});
et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']);