/**
 * 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 LLP
 * 
 * @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,
		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) : 0);
					var2 = (var2 ? parseFloat(var2) : 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();
			
	},
	
	
	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));	
		
		
	},
	
	buildScrollPane : function()
	{
		headings = this.tHead.getElements('th');
		var colspan = headings.length;
		
		var container = new Element('div', {'class':'scroll_pane', 'style':'overflow:auto;'});
		var table = new Element('table');
		
		table.inject(container);

		this.alignHeadings();
		
		var clone = this.tBody.clone(true, true).cloneEvents(this.tBody); ;
		
		this.tBody.empty();
		clone.inject(table);
		var tr = new Element('tr').inject(this.tBody);
		var td = new Element('td',{'colspan': colspan, 'style':'padding:0px;' }).inject(tr);
		
		container.inject(td);
		
		this.tBody.getElements('tr').each(function(el){
			el.getElements('td').each(function(tdEl, i){
				tdEl.set('width', this.max[i] );
			}.bind(this));
		}.bind(this));
		
		if(this.tBody.getElement('tbody')) this.tBody = this.tBody.getElement('tbody');
		this.alignHeadings();
		
	},
	
	
	getColumnWidths : function()
	{
		var max = [];
		
		this.tBody.getElements('tr').each(function(el){
			el.getElements('td').each(function(tdEl, i){
				var size = tdEl.getSize();
				if(size.x > max[i] || !$defined(max[i])) max[i] = size.x;
			});
		});
		
		return max;
	},
	
	alignHeadings : function()
	{
		this.max = this.getColumnWidths();
		
		var headings = this.tHead.getElements('th');
		
		headings.each(function(el,i){
			el.set('width', this.max[i]+'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;
	},
	
	/**
	 * 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();
		}
	});
}

