/**
 * Script: Moosort.js
 * 		HTML table sorting script
 * 
 * License:
 * 		MIT-style license
 * 
 * Copyright:
 * 		2009 - Zohar Arad - www.zohararad.com
 */

/**
 * Class: Moosort
 * 		Sorts an HTML table by column
 * 
 * Implements:
 * 		Mootools.Options
 * 
 * Requires:
 * 		Mootools 1.2 Core with Class, Class.Extras, Element, Element.Events, Element.Style
 * 
 * Options:
 * 		- columnsFormat (Array) - An array of formats corresponding to the sorting format of each table-column.
 * 			Supported formats are 'number','string','date' and null (in case the column is not sortable).
 * 		- dateFormat (String) - Date format for sorting by date.
 * 			Possible values are: 'DD-MM-YYYY', 'MM-DD-YYYY', 'YYYY-MM-DD'.
 * 			Defaults to 'DD-MM-YYYY'.
 *		- profile (Boolean) - If set to true, a profiling console will be displayed above the table. Useful for benchmarking
 * 		- classes (Object) - A collection of CSS classes used to add visual styling to the sorted table:
 * 			- oddrow (Sting) - The CSS class applied to odd table rows.
 * 				Defaults to 'odd'.
 * 			- evenrow (String) - The CSS class applied to even table rows.
 * 				Defaults to 'even'.
 * 			- rowhover (String) - The CSS class  applied to table rows on mouseover.
 * 				Defaults to 'hover'.
 * 			- console (String) - The CSS class of the profiling console element.
 * 				Defaults to 'console'.
 * Example:
 * 		<code>
 * 		var s = new Moosort($('sortable'),{
 * 			columnsFormat:['number','string',null,'date'],
 * 			profile:true
 * 		});
 * 		</code>
 */
var Moosort = new Class({
	Implements: Options,
	options:{
		columnsFormat:[],
		dateFormat:'DD-MM-YYYY',
		profile:false,
		classes:{
			oddrow:'odd',
			evenrow:'even',
			rowhover:'hover',
			console:'console'
		}
	},
	/**
	 * Method: initialize
	 * 		Initializes the Moos class
	 * 
	 * Arguments:
	 * 		options (Object) - See 'Options' above
	 * 
	 */
	initialize:function(table,options){
		this.setOptions(options);
		this.table = $(table);
		this.thead = this.table.getElement('thead');
		this.tbody = this.table.getElement('tbody');
		this.sorters = this.thead.getElements('th');
		this.rows = this.tbody.getElements('tr');
		if(this.options.profile) {
			this.console = new Element('div',{'class':this.options.classes.console}).injectBefore(this.table);
		}
		this.rows.addEvent('mouseover',this.toggleRowHighlight.bindWithEvent(this,['on']));
		this.rows.addEvent('mouseout',this.toggleRowHighlight.bindWithEvent(this,['off']));
		this.initSort();
		this.cacheRows();
	},
	/**
	 * Method: cacheRows
	 * 		Caches the table rows in an internal multi-dimensional array so that sorting will be performed
	 * 		on that array rather than the actual DOM.
	 * 
	 */
	cacheRows:function(){
		this.columns = [];
		this.sorters.each(function(el){
			this.columns.push(new Array());
		},this);
		this.rows.each(function(el,i){
			var tds = el.getElements('td');
			tds.each(function(td,j){
				this.columns[j].push([i,td.get('text')]);
			},this);
		},this);
		this.currentSortColumn = this.columns[0];
	},
	/**
	 * Method: toggleRowHighlight
	 * 		Toggles the row highlight on or off on mouseover and mouseout respectively.
	 * 
	 * Arguments:
	 * 		ev (Event) - Reference to the event that called the method.
	 * 		state (String) - Indicates the 'on' or 'off' state of the highlight
	 * 
	 */
	toggleRowHighlight:function(ev,state){
		el = $(ev.target).get('tag') == 'tr' ? $(ev.target) : $(ev.target).getParent();
		var f = state == 'on' ? function(c){el.addClass(c)} : function(c){el.removeClass(c)};
		f(this.options.classes.rowhover);
	},
	/**
	 * Method: initSort
	 * 		Initializes the sorting action on each of the table columns.
	 */
	initSort:function(){
		this.sorters.each(function(el,i){
			if(this.options.columnsFormat[i] !== null){
				el.store('sort','asc');
				el.compare = function(a,b){
					this.sortIndex = i;
					var a = a[1];
					var b = b[1];
					return this.compare(a,b,el.retrieve('sort'));
				}.bind(this);
				el.addEvent('click',this.sort.bindWithEvent(this,[el,i]));
			}
		},this);
	},
	/**
	 * Method: sort
	 * 		Callback for the sorting action on each of the table columns.
	 * 		Used as an intermidiate interface to allow for visual feedback to the user before the actual
	 * 		sorting takes place.
	 * 
	 * Arguments:
	 * 		ev (Event) - The click event that occured on the relevant table column.
	 * 		el (DOM Element) - Reference to the table column on which the sorting should occur
	 * 		i (Integer) - The index of the sorted column in the columns array
	 */
	sort:function(ev,el,i){
		this.table.set('opacity',0.6);
		this.doSort.delay(100,this,[el,i]);
	},
	/**
	 * Method: doSort
	 * 		Initializes the sorting procedure. Depending on the sort status of the
	 * 		target column either the sorting algorythm is called (first sort) or the row 
	 * 		order in the internal cache is reversed (second sort onwards)
	 * 		
	 * 
	 * Arguments:
	 * 		el (DOM Element) - Reference to the table column on which the sorting should occur
	 * 		i (Integer) - The index of the sorted column in the columns array
	 */
	doSort:function(el,i){
		if(this.options.profile){
			var start = new Date();
		}
		el.store('sort', el.retrieve('sort') == 'asc' ? 'desc' : 'asc');
		el.className = el.retrieve('sort');
		this.currentSortColumn = this.columns[i];
		if (el.retrieve('sorted')) {
			this.currentSortColumn.reverse();
		} else {
			this.currentSortColumn.sort(el.compare);
			el.store('sorted', true);
		}
		this.rebuild();
		this.table.set('opacity',1);
		if (this.options.profile) {
			var end = new Date();
			var msg = 'Sorting took '+(end-start) / 1000 + 'sec.';
			this.console.set('html',msg);
		}
	},
	/**
	 * Method: rebuild
	 * 		Rebuilds the table from the row cache after sorting
	 */
	rebuild:function(){
		this.currentSortColumn.each(function(el,i){
			var row = this.rows[el[0]];
			row.className = i % 2 ? this.options.classes.oddrow : this.options.classes.evenrow;
			this.tbody.adopt(row);
		},this);
	},
	/**
	 * Method: compare
	 * 		Callback for the array.sort algorythm used to sort the table
	 * 
	 * Returns:
	 * 		number (Integer) - either 1 (a > b) or -1 (a < b)
	 */
	compare:function(a,b,dir){
		var sort = this.options.columnsFormat[this.sortIndex];
		switch(sort){
			case 'number':
				a = a.toInt();
				b = b.toInt();
				return dir == 'asc' ? a - b : b - a;
				break;
			case 'string':
				a = a.toLowerCase();
				b = b.toLowerCase();
				if(a==b) return 0;
				else if(a < b && dir == 'asc') return -1;
				else if(b < a && dir == 'desc') return -1;
				else return 1;
				break;
			case 'date':
				a = this.getDate(a);
				b = this.getDate(b);
				return dir == 'asc' ? a - b : b - a;
				break;
		}
	},
	/**
	 * Method: getDate
	 * 		Converts a string-formatted date into a number for date comparison
	 * 
	 * Returns:
	 * 		date (Number) - Numeric representation of a date-formatted string
	 */
	getDate:function(str){
		switch(this.options.dateFormat){
			case 'DD-MM-YYYY':
				var p = str.match(/(\d{1,2})-(\d{1,2})-(\d{2,4})/);
				var date = parseInt(p[3]*10000+p[2]*100+p[1]);
				break;
			case 'MM-DD-YYYY':
				var p = str.match(/(\d{1,2})-(\d{1,2})-(\d{2,4})/);
				var date = parseInt(p[3]*10000+p[1]*100+p[2]);
				break;
			case 'YYYY-MM-DD':
				var p = str.match(/(\d{2,4})-(\d{1,2})-(\d{1,2})/);
				var date = parseInt(p[1]*10000+p[2]*100+p[3]);
				break;
		}
		return date;
	},
	/**
	 * Method: setSortStatus
	 * 		Sets the sort status on a given table column. Used to check whether the column was sorted or not.
	 * 		Can be used to indicate that some columns do not need to sorted using the sort algorythm, thus
	 * 		saving on performance.
	 * 
	 * Arguments:
	 * 		index (Integer) - The index of the column in the columns array
	 * 		status (Boolean) - Whether the column was sorted (true) or not (false)
	 */
	setSortStatus:function(index,status){
		this.sorters[index].store('sorted', status);
	}
});
