/**
 * @copyright Franson Technology AB, Sweden, 2007
 *
 * @author Björn Andersson & Fredrik Blomqvist
 *
 */

if (typeof(Franson) == 'undefined')
{
	/** @type namespace */
	Franson = {};
}
if (typeof(Franson.UI) == 'undefined')
{
	/** @type namespace */
	Franson.UI = {};
}


var defaultTableControlSettings = {
	// style. null = no setting
	TableStyle: null,
	HeaderStyle: null,
	HeaderStyleSelected: null,
	ItemStyle: null,
	SelectedIndexStyle: null,
	AlternatingItemStyle: null,
	//
	PageSize: -1 // = no paging
};


/**
 * todo: add more settings configurations
 * @constructor
 * @param {string} containerId (element/DIV name)
 * @param {object} settings. Optional (not used yet!)
 */
Franson.UI.TableControl = function(containerId, settings)
{
	settings = MochiKit.Base.setdefault(settings, defaultTableControlSettings);

	/** @private @type DOM */
	this.tableDOM = null;
	/** @private @type DOM */
	this.tbodyDOM = null;
 	/** @private @type DOM */
 	this.m_theadDOM = null;

	/**
	 * id of container element (div)
	 * @private @type string
	 */
	this.m_containerId = containerId;

	// Public Properties
	this.DataSource = null;
	/** @type integer */
	this.PageSize = settings.PageSize;

	// @private @type boolean
	this.m_bSelected = false;

	// Style properties
	this.TableStyle = settings.TableStyle;
	this.HeaderStyle = settings.HeaderStyle;
	this.HeaderStyleSelected = settings.HeaderStyleSelected;
	this.ItemStyle = settings.ItemStyle;
	this.AlternatingItemStyle = settings.AlternatingItemStyle;
	this.SelectedIndexStyle = settings.SelectedIndexStyle;
	this.PagerStyle = settings.PagerStyle;

	/** @private @type integer */
	this.m_pageIndex = 0;
	/** @private @type Array[{label: string, getValue: function, createDom: function, cmpValue: comparator}] */
	this.m_mapping = [];
	/** @private @type boolean */
	this.bInitialized = false;

	/** @private @type boolean */
	this.bSortOrderAscending = true; // todo: this could be set per column if comparators where chained

	/** @private @type integer */
	this.m_currColSortIndex = 0; // -1  no sort yet
	/** @private @type integer */
	this.m_iSelectedIndex = -1;

	/**
	 * for event handler GC.
	 * todo: create a better interface so that clients
	 * can give ownership to their events hooked in createDom
	 * (or perhaps supply a destroyFn/destructor field?)
	 * @private @type Array[EventHandler]
	 */
	this.m_events = [];

	/** @private @type boolean */
	this.m_sortDirty = true;
};



Franson.UI.TableControl.prototype =
{
	/**
	 * todo: hmm, let -1 mean last page perhaps?
	 * @param {integer} iNewIndex
	 */
	setPageIndex: function(iNewIndex)
	{
		// set new if not end reached AND paging enabled AND iNewIndex > -1
		if (!((this.PageSize > 0 && iNewIndex > -1) && ((this.PageSize * iNewIndex + this.PageSize) < (this.m_getDataSource().length + this.PageSize))))
			throw new Error("Page index was out of range");

		this.m_pageIndex = iNewIndex;
		this.dataBind();

		// Expose onpage index change event
		MochiKit.Signal.signal(this, "onpageindexchanged", iNewIndex);
	},

	/** @type integer */
	getPageIndex: function()
	{
		return this.m_pageIndex;
	},

	/** @type integer */
	getPageCount: function()
	{
		if (this.DataSource === null)
			return 0;
		if (this.PageSize < 1)
			return 1;

		return Math.ceil(this.DataSource.length / this.PageSize); // DataSource.length == 0 => 0 (ok?)
	},

	/**
	 * @param {integer} iColIndex
	 * @param {boolean} bAsc. Optional (not used now?)
	 */
	setSortColumnIndex: function(iColIndex, bAsc)
	{
		if (iColIndex >= this.m_mapping.length)
			throw new Error("Column index out of range: " + iColIndex);

	//	if (iColIndex == this.m_currColSortIndex)
	//		return;

		// Set current sort column index
		this.m_currColSortIndex = iColIndex;
		this.m_sortDirty = true;
	},

	/** @type integer */
	getSortColumnIndex: function()
	{
		return this.m_currColSortIndex;
	},

	/**
	 * @type DIV
	 */
	getContainer: function()
	{
		return $(this.m_containerId);
	},

	/** @type integer */
	count: function()
	{
		return this.m_getDataSource().length;
	},

	/**
	 *
	 * @param {integer} iIndex
	 */
	setSelectedIndex: function(iIndex)
	{
		this.m_iSelectedIndex = iIndex;
		MochiKit.Signal.signal(this, 'onselectedindexchanged', iIndex);
		if (this.DataSource !== null)
			this.dataBind();
	},

	/** @type integer */
	getSelectedIndex: function()
	{
		return this.m_iSelectedIndex;
	},

	/**
	 * O(N)
	 * @type integer
	 */
	getRowIndexByDataItem: function(dataItem)
	{
		if (this.DataSource === null)
			return -1;

		for (var i = 0; i < this.DataSource.length; ++i)
		{
			if (this.DataSource[i] == dataItem)
			{
				return i;
			}
		}
		return -1;
	},

	/**
	 * @param {integer} rowIndex
	 * @type integer
	 */
	getPageIndexByRowIndex: function(rowIndex)
	{
		if (this.PageSize < 1)
			return 0;
		return Math.floor(rowIndex / this.PageSize); // or return say -1 if out of range? (could be useful to calculate also)
	},

	m_updateSortArrows: function()
	{
		// test arrow chars
	//	var upArrow = String.fromCharCode(8593); // descending // doesn't show correctly in FF?
	//	var downArrow = String.fromCharCode(8595); // ascending
		var upArrow = String.fromCharCode(9650);
		//var upArrow = String.fromCharCode(9652);
		//var upArrow = String.fromCharCode(9651);
		var downArrow = String.fromCharCode(9660);
		//var downArrow = String.fromCharCode(9662); // looks better than 9660 but doesn't show correctly in IE..
		//var downArrow = String.fromCharCode(9661);

		var columnMap = this.m_mapping[this.getSortColumnIndex()];
		var label = columnMap.labelDomNode.nodeValue;
		var arrow = this.bSortOrderAscending ? downArrow : upArrow;

		var currentColumn = false;
		if (label.length > 0)
		{
			var c = label.charAt(label.length-1);
			if (c == upArrow || c == downArrow)
			{
				label = label.substr(0, label.length-1);
				currentColumn = true;
			}
		}
		columnMap.labelDomNode.nodeValue = label + arrow;

		// remove possible arrow from previous column
		if (!currentColumn)
		{
			for (var i = 0; i < this.m_mapping.length; ++i)
			{
				if (i == this.getSortColumnIndex())
					continue;

				var prevLabel = this.m_mapping[i].labelDomNode.nodeValue;
				if (prevLabel.length > 0)
				{
					 var c = prevLabel.charAt(prevLabel.length - 1);
					 if (c == upArrow || c == downArrow)
					 {
						this.m_mapping[i].labelDomNode.nodeValue = prevLabel.substr(0, prevLabel.length - 1);
						break;
					 }
				}
			}
		}
	},

	dataBind: function(optionalDataSource)
	{
		// Create header DOM objects
		if(!this.bInitialized)
		{
			this.m_initTable();
		}

		if (typeof(optionalDataSource) != 'undefined')
		{
			this.DataSource = optionalDataSource;
			if (this.m_iSelectedIndex != -1) // don't change on first run (data will be resorted)
				this.m_iSelectedIndex = this.DataSource.length > 0 ? 0 : -1;
			this.m_sortDirty = true;
		}

		var tempDataSource = null;

		// save selected item to be able to restore after sort
		var currentItem = this.getSelectedItem();
		if (currentItem !== null)
		{
			// note: this could be seen as hackish but otherwise we would need a getStableId(item) to make this stuff work..
			// only case this will fail is if the object in the list are subject to reflection somehow
			// (resorting most likely means the actual object have changed)
			currentItem.__table_ctrl_selected__ = true; // removed after sort
		}

		if (this.m_sortDirty)
		{
			var columnMap = this.m_mapping[this.getSortColumnIndex()];
			var sign = this.bSortOrderAscending ? 1 : -1;

			var cmpFn = columnMap.cmpValue;
			if (cmpFn === null)
			{
				// if no user-supplied cmp use standard compare on value field.
				// (note that a user supplied cmp gets the full data-item as argument)
				cmpFn = function(a, b)
				{
					return MochiKit.Base.compare(columnMap.getValue(a), columnMap.getValue(b));
				};
			}

			tempDataSource = this.m_getDataSource().sort(
				function(a, b)
				{
					return sign * cmpFn(a, b);
				}
			);

			this.m_sortDirty = false;

			this.m_updateSortArrows();
		}
		else
		{
			tempDataSource = this.m_getDataSource();
		}

		// restore previously selected item
		if (currentItem !== null)
		{
			this.m_iSelectedIndex = -1;
			for (var i = 0; i < tempDataSource.length; ++i)
			{
				if (typeof(tempDataSource[i].__table_ctrl_selected__) != 'undefined')
				{
					this.m_iSelectedIndex = i;
					break;
				}
			}
			delete currentItem.__table_ctrl_selected__;
		}

		if (this.m_pageIndex > this.getPageCount() - 1)
			this.m_pageIndex = Math.max(0, this.getPageCount() - 1); // can't use setPageIndex since it trigger dataBind!

		if (this.PageSize < 1)
		{
			// paging disabled --> draw all
			this.m_dataBind(tempDataSource, 0, tempDataSource.length);
		}
		else
		{
			this.m_dataBind(
				tempDataSource,
				this.PageSize * this.m_pageIndex,
				(this.PageSize * this.m_pageIndex) + this.PageSize
			);
		}

		MochiKit.Signal.signal(this, 'ondatabind');
	},

	/**
	 *
	 * @param {integer} iIndex
	 * @throws Error
	 */
	removeItem: function(iIndex)
	{
		if (!(iIndex > -1 && iIndex < this.m_getDataSource().length))
			throw new Error("Index: " + iIndex + " was out of range");

		var arr = this.m_getDataSource();
		arr.splice(iIndex, 1);
		this.DataSource = arr;
	},

	/**
	 *
	 * @param {integer} iIndex
	 * @throws Error
	 */
	getItem: function(iIndex)
	{
		if(!(iIndex > -1 && iIndex < this.m_getDataSource().length))
			throw new Error("Index: " + iIndex + " was out of range");

		return this.m_getDataSource()[iIndex];
	},

	getSelectedItem: function()
	{
		if (this.m_iSelectedIndex == -1)
			return null;
		return this.getItem(this.m_iSelectedIndex);
	},

	/**
	 * The header text, function for setting value, function to create DOM
	 * note: can also take a single literal {label, getValue, createDom, cmpValue, tooltip}
	 * @param {string} strLabel
	 * @param {function} funcGetValue
	 * @param {function} funcCreateDom Optional. default createTextNode
	 * @param {function} cmpValue Optional binary comparator. default '<'
	 */
	registerColumn: function(strLabel, funcGetValue, funcCreateDom, cmpValue)
	{
		var fields = null;
		if (arguments.length == 1) // new interface. (assume a single literal fields specifier)
		{
			fields = arguments[0];
		}
		else // old interface (to maintain backwards compatibility) todo: deprecate this
		{
			fields = {
				'label': strLabel,
				'getValue': funcGetValue,
				'createDom': funcCreateDom,
				'cmpValue': cmpValue
			};
		}

		// Map a colname with a func to retrieve a value etc (and set defaults)
		this.m_mapping.push({
			'label': fields.label,
			'getValue': fields.getValue || function(item) { return item; }, // default: passthrough (NOP)
		//	'createDom': fields.createDom || MochiKit.Base.method(document, document.createTextNode), // default TextNode // !Mochi.method doesn't seem to work on document in IE! todo: file bugreport to Mochi
			'createDom': fields.createDom || function(text) { return document.createTextNode(text); }, // default TextNode
			'cmpValue': fields.compare || null, // MochiKit.Base.compare // default value comp (item cmp if no getValue)
			'tooltip': fields.tooltip,
			'nosort': (typeof(fields.nosort) != 'undefined') ? fields.nosort : (typeof(fields.getValue) == 'undefined' && typeof(fields.compare) == 'undefined')
		});
	},

	/**
	 * @param {Array[{label: string, getValue: function, createDom: function, cmpValue: function}]}
	 */
	registerColumns: function(columns)
	{
		for (var i = 0; i < columns.length; ++i)
		{
			this.registerColumn(columns[i]);
		}
	},

	/**
	 * Convenience sort: sets the sort col, sort dir and rebind the control
	 * @param {integer} iColIndex
	 */
	sort: function(iColIndex)
	{
		if (this.m_mapping[iColIndex].nosort)
			return;

		this.setSortColumnIndex(iColIndex); // this sets the dirty-flag
		this.dataBind();
	},

	/**
	 * @private
	 * @param {Object} arrDataSource
	 * @param {integer} iRangeFrom
	 * @param {integer} iRangeTo
	 */
	m_dataBind: function(arrDataSource, iRangeFrom, iRangeTo)
	{
		// Clear all current rows
		this.m_clearRows();

		for (var i = iRangeFrom; i < arrDataSource.length && i < iRangeTo; ++i)
		{
			var tableRow = document.createElement('tr');

			for (var j = 0; j < this.m_mapping.length; ++j)
			{
				var tableCol = document.createElement('td');
				var value = this.m_mapping[j].getValue(arrDataSource[i]);

				var valueNode = this.m_mapping[j].createDom(value); // todo: hmm, or should we always pass the entire item? (or perhaps both item and value?)

				tableCol.appendChild(valueNode);
				tableRow.appendChild(tableCol);
			}

			// Set styles
			if(this.AlternatingItemStyle !== null && (i + 1) % 2 == 1)
			{
				tableRow.className = this.AlternatingItemStyle;
			}

			if(this.ItemStyle !== null)
			{
				tableRow.className = this.ItemStyle;
			}

			if (i == this.getSelectedIndex() && this.SelectedIndexStyle !== null)
			{
				tableRow.className = this.SelectedIndexStyle;
			}

			this.tbodyDOM.appendChild(tableRow); // or perhaps first create all rows before hooking to tbodyDom? (faster?)

			var eh = MochiKit.Signal.connect(tableRow, 'onclick',
				MochiKit.Base.method(this, function(i, selectedRow, e)
				{
					this.setSelectedIndex(i);
					MochiKit.Signal.signal(this, 'onrowclicked', i);
				}, i, tableRow)
			);
			this.m_events.push(eh); // add to GC pool
		}

		// Set new source
		this.DataSource = arrDataSource;
	},

	/**
	 * @private
	 */
	m_initTable: function()
	{
		if (this.bInitialized)
			return;

		this.tableDOM = document.createElement('table');
		this.tableDOM.setAttribute('cellspacing', '0');
		this.tableDOM.setAttribute('cellpadding', '0');

		// Set styles
		if(this.TableStyle !== null)
		{
			this.tableDOM.className = this.TableStyle;
		}

		this.getContainer().appendChild(this.tableDOM);

		this.m_createHeaders();

		this.m_createBody();

		if (this.PageSize > 0)
			this.m_createPagingControl();

		this.bInitialized = true;
	},


	// todo: add goto direct page (select list) also
	// todo: make more configurable (styles, text etc)
	// todo: this kind of control is generic enought that it could be extracted as a separate module perhaps?
	m_createPagingControl: function()
	{
		// should perhaps expose next/prev-Page methods?
		function onPageChangeButtonClick(delta)
		{
			var iCurrentPageIndex = this.getPageIndex();
			var newIndex = iCurrentPageIndex + delta;
			if (0 <= newIndex && newIndex < this.getPageCount())
				this.setPageIndex(newIndex);
		}

		var gotoPrevPage = MochiKit.Base.method(this, onPageChangeButtonClick, -1);
		var gotoNextPage = MochiKit.Base.method(this, onPageChangeButtonClick, +1);

		// todo: expose a native setLatPage? (or make setPage(-1) mean last?)
		var gotoLastPage = MochiKit.Base.method(this, function()
		{
			if (this.getPageCount() > 0)
			{
				this.setPageIndex(this.getPageCount() - 1);
			}
		});

		// alias
		var createDom = MochiKit.DOM.createDOM;

		var TABLE = MochiKit.DOM.TABLE;
		var TR = MochiKit.DOM.TR;
		var TD = MochiKit.DOM.TD;
		var BUTTON = MochiKit.DOM.BUTTON;
		var INPUT = MochiKit.DOM.INPUT;
		var SPAN = MochiKit.DOM.SPAN;
		var SELECT = MochiKit.DOM.SELECT;
		var THEAD = MochiKit.DOM.THEAD;

		// todo: add better looking html-nodes instead of ascii '<<'
		// hmm, _sometimes_ these buttons trigger a page-refresh.. stick to INPUT-buttons for now..
//		this.firstPageBtn= BUTTON({ title: 'first page', 'onclick': MochiKit.Base.bind(this.setPageIndex, this, 0) }, '[<<');
//		this.prevPageBtn = BUTTON({ title: 'previous page', 'onclick': gotoPrevPage }, '<');
//		this.nextPageBtn = BUTTON({ title: 'next page', 'onclick': gotoNextPage }, '>');
//		this.lastPageBtn = BUTTON({ title: 'last page', 'onclick': gotoLastPage }, '>>]');

		this.firstPageBtn= INPUT({ type: 'button', value: '[<<', title: 'first page', 'onclick': MochiKit.Base.method(this, this.setPageIndex, 0) });
		this.prevPageBtn = INPUT({ type: 'button', value: '<', title: 'previous page', 'onclick': gotoPrevPage });
		this.nextPageBtn = INPUT({ type: 'button', value: '>', title: 'next page', 'onclick': gotoNextPage });
		this.lastPageBtn = INPUT({ type: 'button', value: '>>]', title: 'last page', 'onclick': gotoLastPage });

		// MochiKit.DOM.setElementClass(button, this.PageingButtonStyle); ?

		this.pageIndexDiv = SPAN({ title: 'number of pages' }, '');

		var gotoPageIndex = MochiKit.Base.method(this, function()
		{
			this.setPageIndex(this.pageIndexSelect.selectedIndex);
		});

		this.pageIndexSelect = SELECT({ title: 'go to page', 'onchange': gotoPageIndex });

		this.buttonTable = TABLE(typeof(this.PagerStyle) != 'undefined' ? {'class': this.PagerStyle} : null,
			THEAD(null, // IE seems to need either a thead or tbody to make this work correctly..
				TR(null,
					TD(null, this.firstPageBtn),
					TD(null, this.prevPageBtn),
					TD(null, this.nextPageBtn),
					TD(null, this.lastPageBtn),

					TD(null, this.pageIndexSelect), // or put this in the middle?

					TD(null, this.pageIndexDiv)
				)
			)
		);

		MochiKit.Style.hideElement(this.buttonTable); // set initially hidden

		this.getContainer().appendChild(this.buttonTable);

		MochiKit.Signal.connect(this, 'ondatabind', this,
			function()
			{
				var numPages = this.getPageCount();

				if (numPages > 1)
				{
					this.prevPageBtn.disabled = false;
					this.nextPageBtn.disabled = false;
					this.firstPageBtn.disabled = false;
					this.lastPageBtn.disabled = false;

					var pageIndex = this.getPageIndex();
					if (pageIndex === 0)
					{
						this.prevPageBtn.disabled = true;
						this.firstPageBtn.disabled = true;
					}
					if (pageIndex == numPages - 1)
					{
						this.nextPageBtn.disabled = true;
						this.lastPageBtn.disabled = true;
					}

					if (this.pageIndexSelect.options.length != numPages)
					{
						this.pageIndexSelect.options.length = 0;
						// better?
					//	while (this.pageIndexSelect.options.length > 0)
					//		this.pageIndexSelect.options[0] = null;

						for (var i = 0; i < numPages; ++i)
						{
							this.pageIndexSelect[this.pageIndexSelect.length] = new Option(i + 1, i);
						}
					}
					this.pageIndexSelect.selectedIndex = pageIndex;

				//	this.pageIndexDiv.innerHTML = (numPages > 0) ? ((pageIndex + 1) + '/' + numPages) : '';
					this.pageIndexDiv.innerHTML = '/ ' + numPages; // + '    -- ' + (pageIndex*this.PageSize + 1) + ' to ' + Math.min(this.count(), ((pageIndex+1)*this.PageSize)) + ' of ' + this.count();

					MochiKit.Style.showElement(this.buttonTable);
				}
				else
				{
					MochiKit.Style.hideElement(this.buttonTable);
				}
			}
		);
	},

	/**
	 * @private
	 */
	m_createBody: function()
	{
		// appends a <tbody> to the table
		this.tbodyDOM = document.createElement('tbody');
		this.tableDOM.appendChild(this.tbodyDOM);
	},

	/**
	 * @private
	 */
	m_createHeaders: function()
	{
		var theadNode = document.createElement('thead');
		this.m_theadDOM = document.createElement('tr');

		// Go through the registered columns and create DOM objs and set the header text
		for (var i = 0; i < this.m_mapping.length; ++i)
		{
			var headerCol = document.createElement('td');
			if (typeof(this.m_mapping[i].tooltip) != 'undefined')
				headerCol.title = this.m_mapping[i].tooltip;
			var headerText = document.createTextNode(this.m_mapping[i].label); // todo: keep a hook to this to be able to set sort-order arrow indicators
			this.m_mapping[i].labelDomNode = headerText;

			// todo. add this to CSS (set in 'onclick' below) (+ clear previous)
		//	if (this.HeaderColumnStyleSelected !== null)
		//	{
		//		headerCol.className	= this.HeaderColumnStyleSelected;
		//	}

			MochiKit.Signal.connect(headerCol, 'onclick',
				MochiKit.Base.method(this, function(index, e)
				{
					if (!this.m_mapping[index].nosort)
					{
						if (this.getSortColumnIndex() == index) // invert sort order if same column twice
						{
							this.bSortOrderAscending = !this.bSortOrderAscending;

							if (this.DataSource !== null)
							{
								if (this.DataSource.length > 0)
								{
									// special case hack to make inverted sort order of current column fast
									// todo: this works but doesn't update the sort-arrows...
									var selectedIndex = this.m_iSelectedIndex;
									this.DataSource.reverse();
									if (selectedIndex != -1)
									{
										this.m_iSelectedIndex = this.DataSource.length - selectedIndex - 1;

										// todo: is this good? need better logic.. sometimes you want this, sometimes you don't..
										// goto page where selected item is (now)
										// ok? or set pageIndex to 0 when binding to new data?
									//	var pageIdx = this.getPageIndexByRowIndex(this.m_iSelectedIndex);
									//	this.m_pageIndex = pageIdx;
									}
									this.dataBind();
								}
								this.m_updateSortArrows(); // since sort is not dirty databind will not call this
							}
						}
						else // on changed column start with ascending
						{
							this.bSortOrderAscending = true;
							this.sort(index);
						}
					}

					MochiKit.Signal.signal(this, 'onheaderclicked', index);
				}, i)
			);

			headerCol.appendChild(headerText);
			this.m_theadDOM.appendChild(headerCol);
		}

		this.setHeaderActive(this.m_bSelected);

		theadNode.appendChild(this.m_theadDOM);
		this.tableDOM.appendChild(theadNode);
	},

	/**
	 * @private
	 */
	m_getDataSource: function()
	{
		return this.DataSource;
	},

	///
	/// Table editing functions
	///

	/**
	 * @param {boolean} bActive
	 */
	setHeaderActive: function(bActive)
	{
		this.m_bSelected = bActive;

		if(this.m_theadDOM !== null)
		{
			if(this.m_bSelected)
			{
				// Set styles
				if(this.HeaderStyleSelected !== null)
				{
					MochiKit.DOM.setElementClass(this.m_theadDOM, this.HeaderStyleSelected);
				}
			}
			else
			{
				// Set styles
				if(this.HeaderStyle !== null)
				{
					MochiKit.DOM.setElementClass(this.m_theadDOM, this.HeaderStyle);
				}

				// remove selected row indication
				// (hmm, or should we store this to be able to set it again when the table-header becomes active?)
				this.setSelectedIndex(-1);
			}
		}
	},

	/**
	 * @private
	 */
	m_clearRows: function()
	{
		if (!this.bInitialized)
			throw new Error("Table not initialized");

		// cleanup events to avoid leaks
		for (var i = 0; i < this.m_events.length; ++i)
			MochiKit.Signal.disconnect(this.m_events[i]);
		this.m_events = [];

		this.tableDOM.removeChild(this.tbodyDOM);
		this.tbodyDOM = null; // avoid leak (?)
		this.m_createBody();
	}

};
