/**
 * Table sorting class
 * 
 * Applies click events to the
 * <th> elements which carry the functions to perform the Array.sort() methods
 * 
 * @requires Mootools-1.2
 * 
 * @author Lee Hoang
 * @author Ollie Maitland
 * @copyright Byng Systems Limited
 * 
 * @see http://www.phatfusion.net/sortabletable/
 */

Byng.register('byng.table.sort', {
	Implements : [ Events, Options ],

	/**
	 * Set sort options
	 * 
	 * @param Object
	 */
	options : {
		overClass : false,
		onClick : false,
		sortable : true,
		scrollable : false,
		sortOn : 0,
		sortBy : 'ASC',
		grouped : false,
		groupable : false,
		onGroup : null,
		grouping : {
			'axis' : null
		}
	},

	/**
	 * Initialise source table and request define sort algorithm for table
	 * 
	 * @param DomElement
	 *            sourceTable
	 * @param Object
	 *            options
	 */
	initialize : function(sourceTable, options) {
		options = options || {}
		this.setOptions(options);
		this.sourceTable = $(sourceTable);
		if (this.options.scrollable) {
			this.sourceTable.addEvent('modify', function(e) {
				this.alignHeadings();
				this.buildEvents();
			}.bind(this));
		}

		// load the <td> elements
		this.loadElements(options);

		if (options.groupable == true) {
			this.group();
		}

		// store a reference to this object
		this.sourceTable.store('table', this);
	},

	/**
	 * Attach behaviour to the header rows
	 * 
	 * @param DomElement
	 *            el
	 * @param Integer
	 *            i Cell index
	 */
	attachBehaviour : function(el, i) {
		// condition: axis attribute set
		if (el.axis && this.options.sortable) {

			// remove any previous event assignment
			el.removeEvents();

			// add the click event
			el.addEvent('click', this.orderRows.bind(this, i));
			// mouse over method
			el.addEvent('mouseover', function() {
				el.addClass('table_header_over');
			});
			// mouseout event
			el.addEvent('mouseout', function() {
				el.removeClass('table_header_over');
			});

			// attach date utility method by reference
			el.getDate = this.getDate;

			/**
			 * Locate the deepest text node
			 * 
			 * @param DomElement
			 *            element
			 * @return String
			 */
			el.findData = function(elem) {
				// check if there is a child ement
				if (elem.firstChild) {
					return this.findData(elem.firstChild);
				} else {
					return (elem.textContent ? elem.textContent.trim()
							: (elem.data ? elem.data.trim() : ''));
				}
			}.bind(el);

			/**
			 * Compare function for element sort
			 * 
			 * @param DomElement
			 *            a
			 * @param DomElement
			 *            b
			 * @returns Integer
			 */
			el.compare = function(a, b) {

				if (a.getElementsByTagName('td')[i]) {
					// get the data of the two elements to compare
					var1 = $(a.getElementsByTagName('td')[i]).retrieve(
							'textContent');
					if (var1 == null) {
						var1 = this.findData(a.getElementsByTagName('td')[i]);
						$(a.getElementsByTagName('td')[i]).store('textContent',
								var1);
					}
				} else {
					var1 = null;
				}

				if (b.getElementsByTagName('td')[i]) {
					// get the data of the two elements to compare
					var2 = $(b.getElementsByTagName('td')[i]).retrieve(
							'textContent');
					if (var2 == null) {
						var2 = this.findData(b.getElementsByTagName('td')[i]);
						$(b.getElementsByTagName('td')[i]).store('textContent',
								var2);
					}
				} else {
					var2 = null;
				}

				/**
				 * Sort numbers
				 * 
				 */
				if (this.axis == 'number') {

					var1 = (var1 ? parseFloat(var1.replace(',', '')) : 0);
					var2 = (var2 ? parseFloat(var2.replace(',', '')) : 0);
					
					if (this.sortBy == 'ASC') {
						return var1 - var2;
					} else {
						return var2 - var1;
					}
					/**
					 * Sort strings
					 * 
					 */
				} else if (this.axis == 'string') {
					var1 = (var1 ? var1.toUpperCase() : "");
					var2 = (var2 ? var2.toUpperCase() : "");

					if (var1 == var2) {
						return 0
					}
					;
					if (this.sortBy == 'ASC') {
						if (var1 < var2) {
							return -1
						}
						;
					} else {
						if (var1 > var2) {
							return -1
						}
						;
					}
					return 1;
					/**
					 * Sort dates
					 * 
					 */
				} else if (this.axis == 'date') {
					var1 = (var1 ? parseFloat(this.getDate(var1)) : 0);
					var2 = (var2 ? parseFloat(this.getDate(var2)) : 0);

					if (this.sortBy == 'ASC') {
						return var1 - var2;
					} else {
						return var2 - var1;
					}
					/**
					 * Sort currency
					 * 
					 */
				} else if (this.axis == 'currency') {
					var1 = (var1 ? parseFloat(var1.substr(1).replace(',', ''))
							: 0);
					var2 = (var2 ? parseFloat(var2.substr(1).replace(',', ''))
							: 0);

					if (this.sortBy == 'ASC') {
						return var1 - var2;
					} else {
						return var2 - var1;
					}
				}
			}.bind(el);

			if (i == this.options.sortOn) {
				el.fireEvent('click');
			}
		}
	},

	/**
	 * Load the row elements
	 * 
	 * @param Object
	 *            options
	 */
	loadElements : function(options) {
		if (!options)
			options = this.options;

		// set the class references to the table elements
		this.tHead = this.sourceTable.getElement('thead');
		this.tBody = this.sourceTable.getElement('tbody');
		this.tFoot = this.sourceTable.getElement('tfoot');

		if (this.options.scrollable) {
			this.buildScrollPane();
		}

		this.buildEvents();

		this.altRow();

	},

	/**
	 * Build the events for the table
	 * 
	 */
	buildEvents : function() 
	{
		this.elements = this.tBody.getElements('tr');

		// attach the hover events
		this.elements.each(function(el, i) {
			if (this.options.overClass) {
				el.addEvent('mouseover', function() {
					el.addClass(options.overClass);
				}, this);
				el.addEvent('mouseout', function() {
					el.removeClass(options.overClass);
				});
			}
			if (this.options.onClick) {
				el.addEvent('click', options.onClick);
			}
		}, this);

		// Attached events to the header rows
		this.tHead.getElements('th').each(this.attachBehaviour.bind(this));

	},

	/**
	 * Build the scrollable pane window
	 * 
	 * Put the tbody into a scrollable <div> within a <td>
	 * Allows <thead> and <tfoot> to remain at the top of the screen
	 * 
	 */
	buildScrollPane : function() 
	{	
		headings = this.tHead.getElements('th');
		var colspan = headings.length;

		var container = new Element('div', {
			'class' : 'scroll_pane',
			'style' : 'overflow:auto;'
		});

		// create a new table
		var table = new Element('table');
		table.inject(container);

		// and a new tbody
		var newTbody = new Element('tbody');
		newTbody.inject(this.tHead,'after');
		
		// add an empty <td> into the old table
		var tr = new Element('tr').inject(newTbody);
		var td = new Element('td', {
			'colspan' : colspan,
			'style' : 'padding:0px;'
		}).inject(tr);
		
		// and put the tBody into our new table
		container.inject(td);
		this.tBody.inject(table);

		// align the headings
		this.alignHeadings();
	},

	/**
	 * Get widths from the first row
	 * 
	 *  @return Array
	 */
	getColumnWidths : function() 
	{
		var widths = [];

		// get widths from first row
		this.tBody.getElement('tr').getElements('td').each(
			function(td, i) {
				widths[i] = td.getSize().x;
//				var size = td.getSize();
//				find the maximum in the table (removed for speed optimisation)
//				if (size.x > widths[i] || !$defined(widths[i]))
//					widths[i] = size.x;
			});
		
		return widths;
	},

	/**
	 * Align headings with the table contents
	 * 
	 * Useful when the tHead and tBody are not in the
	 * same table (i.e when in scrollable </div>)
	 * 
	 */
	alignHeadings : function() 
	{
		var widths = this.getColumnWidths();
		
		if(this.tBody.getElement('tr').getElements('td').length > 1){
			this.tHead.getElements('th').each(function(el, i) {
				el.setStyle('width', '' + (widths[i] - 10) + 'px');
			}.bind(this));

			
			this.tHead.getElements('th span').each(function(el, i) {
				el.setStyle('width', '' + (widths[i] - 10) + 'px');
			}.bind(this));
		}
	},

	/**
	 * Get date utility method
	 * 
	 * @param String
	 *            str
	 */
	getDate : function(str) {
		if (!str)
			return null;

		function fixYear(yr) {
			yr = +yr;
			if (yr < 50) {
				yr += 2000;
			} else if (yr < 100) {
				yr += 1900;
			}
			return yr;
		}
		;
		var ret;

		if (str.length > 12) {
			strtime = str.substring(str.lastIndexOf(' ') + 1);
			strtime = strtime.substring(0, 2) + strtime.substr(-2)
		} else {
			strtime = '0000';
		}

		if (ret = str.match(/(\d{2,4})-(\d{1,2})-(\d{1,2})/)) {
			return (fixYear(ret[1]) * 10000) + (ret[2] * 100) + (+ret[3])
					+ strtime;
		}

		if (ret = str.match(/(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})/)) {
			return (fixYear(ret[3]) * 10000) + (ret[2] * 100) + (+ret[1])
					+ strtime;
		}
		return 999999990000; // So non-parsed dates will be last, not first
	},

	/**
	 * Sort the table ascending/descending
	 * 
	 * @param Int
	 *            index
	 */
	orderRows : function(index) {
		this.options.sortOn = index;
		var header = this.tHead.getElements('th');
		var el = header[index];

		// remove the previous classes
	header.each(function(e, i) {
		if (i != index) {
			e.removeClass('sorted_asc');
			e.removeClass('sorted_desc');
		}
	});

	// toggle the sorting order
	if (el.hasClass('sorted_asc')) {
		el.removeClass('sorted_asc');
		el.addClass('sorted_desc');
		el.sortBy = 'DESC';
	} else if (el.hasClass('sorted_desc')) {
		el.removeClass('sorted_desc');
		el.addClass('sorted_asc');
		el.sortBy = 'ASC';
	} else {
		if (this.options.sortBy == 'ASC') {
			el.addClass('sorted_asc');
			el.sortBy = 'ASC';
		} else if (this.options.sortBy == 'DESC') {
			el.addClass('sorted_asc');
			el.sortBy = 'DESC';
		}
	}

	this.elements.sort(el.compare);
	this.altRow();
	this.elements.injectInside(this.tBody);
},

/**
 * Add/remove class for table rows
 * 
 */
altRow : function() {
	var j = 0;
	this.elements.each(function(el, i) {
		if (el.getStyle('display') != 'none')
			j++;
		if (j % 2)
			el.removeClass('altRow');
		else
			el.addClass('altRow');
	});
},

/**
 * Group elements by an axis
 * 
 * @param {Object}
 *            e
 */
group : function(e) {
	// determin the axis to group by
	e = (($type(e) == 'event') ? e.target : e);
	if ($type(e) == 'element') {
		var currentGrouping = this.options.grouping.axis;
		if ( [ 'input', 'select' ].contains(e.get('tag'))) {
			this.options.grouping.axis = parseInt(e.get('value'));
		}
		// condition: grouping by the same axis
	if (currentGrouping == this.options.grouping.axis)
		return;
}

// condition: axis not set so expand all
if (!$chk(this.options.grouping.axis)) {
	if (!this.options.grouped)
		return;
	this.elements.removeClass('hide');
	(this.elements.getElements('.icon') || []).each(function(i) {
		i.destroy();
	});
	return this.altRow();
}

// condition: grouping by a different axis
if ((this.options.grouping.axis != this.options.sortOn)
		&& this.options.sortable == true) {
	// order the rows
	this.orderRows(this.options.grouping.axis);
}

var thisContent, lastContent, tree;
this.toggleElement = new Element('a', {
	'class' : 'icon expand',
	'href' : '#'
}).set('html', '<span>+</span>');

var findData = function(elem) {
	// check if there is a child ement
	if (elem.firstChild) {
		return findData(elem.firstChild);
	} else {
		return (elem.textContent ? elem.textContent.trim()
				: (elem.data ? elem.data.trim() : ''));
	}
}

// start group function
var startGroup = function(tr) {
	lastContent = thisContent;
	return (tree = tr.removeClass('hide').store('tree', []).store('treeState',
			false));
}

// loop: <tr> elements
this.elements.each(function(tr) {

	// condition: content the same as the previous row
		thisContent = $(tr.cells[this.options.grouping.axis]).retrieve(
				'textContent');
		if (thisContent == null) {
			thisContent = findData(tr.cells[this.options.grouping.axis]);
			tr.cells[this.options.grouping.axis].store('textContent',
					thisContent);
		}

		if (thisContent == null)
			return;
		if (lastContent == null)
			return startGroup(tr);

		if (lastContent == thisContent) {
			// hide this row as matched previous row
			tree.retrieve('tree').push(tr.addClass('hide'));
			(tr.getElements('.icon') || []).each(function(i) {
				i.destroy();
			});
		} else {

			// add the toggle element into the first column
			if (tree.retrieve('tree').length > 0) {
				this.toggleElement.clone().addEvent('click',
						this.toggleGroup.bindWithEvent(this, tree)).inject(
						$(tree.cells[0]).empty());
			}
			startGroup(tr);
		}

	}.bind(this));

// add the toggle element into the first column
if (tree.retrieve('tree').length > 0) {
	this.toggleElement.clone().addEvent('click',
			this.toggleGroup.bindWithEvent(this, tree)).inject(
			$(tree.cells[0]).empty());
}

// highlight the rows with the grouping
this.altRow();
this.options.grouped = true;
this.fireEvent('group', e);
},

/**
 * Toggle a group
 * 
 * @param {Object}
 *            e
 * @param {Object}
 *            target
 */
toggleGroup : function(e, target) {
e.stop();
var state = target.retrieve('treeState');
var tree = target.retrieve('tree');
var refreshRowHighlight = false;
// loop: tree children
	(tree || []).each(function(tr) {

		// condition: our tree has been left outside of the grouped rows (e.g.
		// sorted or new rows added)
			if (tr.rowIndex > target.rowIndex + tree.length
					|| tr.rowIndex < target.rowIndex) {
				// move inside the row set
				tr.inject(
						target.getParent('tbody').rows[(target.rowIndex - 1)],
						'after');
				refreshRowHighlight = true;
			}
			(state ? tr.addClass('hide') : tr.removeClass('hide'));
		});
	target.store('treeState', !state);
	(state ? e.target.removeClass('collapse') : e.target.addClass('collapse'));
	if (refreshRowHighlight == true)
		this.altRow();
}

});

if (typeof (ByngTable) != "undefined") {

	// implement ByngTable.sortable( boolean ) method
	ByngTable.implement( {
		/**
		 * Make sortable
		 * 
		 * @param Boolean
		 *            state
		 */
		setSortable : function(state) {
			if (state == true) {
				this.__sorter = Byng.init('byng.table.sort', this.getTable());
			} else {
				this.__sorter = null;
			}
		},

		getSorter : function() {
			return this.__sorter;
		},

		/**
		 * Refresh the elements
		 * 
		 * @return void
		 */
		refreshSortables : function() {
			this.__sorter.loadElements();
		}
	});
}

