/***************************************************************************
 * mootools-mootable-2.0.js
 * ---------------
 *   author		-> Tyson Cox
 *   started	-> Friday, Aug 6th, 2010
 *   modified	-> Friday, Aug 6th, 2010
 *   copyright	-> 
 *   email     	-> tyson@brightlabs.com.au
 *   version    -> 1.0.1
 *
 * file description
 * ------------------
 * Provides a dynamically generated table allowing sorting and filtering
 *
 *
 * change log
 * ------------------
 * 1.0			Initial development
 * 1.0.1		Added more intelligent pagination
 *
 ***************************************************************************/


mooTable2 = new Class({
	
	Implements: [Options,Events],
	
	options: {	'replaces':	null,
				'updateType': 'replacement',		// replacement, ajax-html, ajax-json
				'requestUrl': null,
				'requestType': null,
				'doInitialRequest': false,
				'useSpinner': false,
				'spinnerTarget': null,
				'spinnerMessage': null,
				'spinnerFxTime': 250,
				'spinnerFxTransition':	Fx.Transitions.Quad.easeInOut,
				
				'topPagination': false,
				
				'sortMode': null,
				'sortDir': 'asc',
				'currentPage': 1,
				'totalResults': 0, 
				'rowsPerPage': 2,
				
				'zebraSize': 2,
				'pageMod': 5,
				'pageModClose': 5,
				'pageModThreshold': 20,
				
				'funcDataLoaded': null,
				
				'funcRowCreate': null,
				'funcRowOver': null,
				'funcRowOff': null,
				'funcRowClick': null,
				
				'funcRenderCompleted': null
	},
	
					/*--------------------------------------------------------o
	----------------\                     Member Variables                    |
		variables    \-------------------------------------------------------*/
					
	tableFrame: null,
	mooTable: null,
	mooTBody: null,
	
	headerItems: {},
	columnCount: 0,
	dataItems: Array(),
	currentIndex: 0,
	
	
	
					/*--------------------------------------------------------o
	----------------\                    Internal Functions                   |
		 internal    \-------------------------------------------------------*/
		
	initialize: function(elem, options) {
		this.setOptions(options);
		this.tableFrame = elem;
		
		// Initial data propagation mode
		if ( this.options.updateType == 'replacement' ) {
			this.parseTable(this.options.replaces)
		}
		
		// Remove replacement if necessary
		if ( this.options.replaces ) {
			this.options.replaces.dispose();
		}
		
		
		if ( this.options.updateType == 'replacement' ) {
			// Render the table
			this.render();
		} else {
			this.checkData();
		}
	},

					/*--------------------------------------------------------o
	----------------\                   List Setup Functions                  |
		  setup      \-------------------------------------------------------*/
	
	addHeader: function(text, sortable, sortcode) {
		this.headerItems[sortcode] = {	'text':			text,
										'sortable':		sortable,
										'sortcode':		sortcode
										};
		this.columnCount++;
		
		return true;
	},
	
	
	addRow: function(values, idcode) {
		// Default values
		if ( idcode == null ) {			idcode = 0;		}
	
		this.dataItems[this.currentIndex] = values;
		this.currentIndex++;
		
		return true;
	},
	
	
	
					/*--------------------------------------------------------o
	----------------\                  Initial Table Parsing                  |
		 parsing     \-------------------------------------------------------*/
	
	parseTable: function(table) {
		var mootable = this;
		
		// Parse out our headers
		var tmp_headers = table.getElements('th');
		
		tmp_headers.each(function(item, idx) {
			mootable.addHeader(item.get('html'), true, 'col-'+idx);
		});
		
		
		// Now parse out our rows
		var tmp_rows = table.getElements('tr');
		var data_rows = 0;
		
		tmp_rows.each(function(row, idx) {
			if ( !row.hasClass('mootable-ignore') ) {
				var tmp_cells = row.getElements('td');
				var data_count = 0;
				var values = {};
				
				tmp_cells.each(function(cell, cellIdx) {
					if ( cell.get('html') == parseFloat(cell.get('html'), 10) ) {
						values['col-'+cellIdx] = parseFloat(cell.get('html'), 10);
					} else {
						values['col-'+cellIdx] = cell.get('html');
					}
					data_count++;
				});
				
				if ( data_count ) {
					mootable.addRow(values, 0);
					data_rows++;
				}
			}
		});
		
		this.options.totalResults = data_rows;
		
		return true;
	},
					
	
	
					/*--------------------------------------------------------o
	----------------\                      List Rendering                     |
		  render     \-------------------------------------------------------*/
	
	checkData: function() {
		switch ( this.options.updateType ) {
			case 'ajax-html':
				//break;
			case 'ajax-json':
				this.updateData();
				break;
			default:
				this.render();
				break;
		}
	},
	
					
	render: function() {
		if ( !this.tableFrame ) {
			return;
		}
			
		// We want to get rid of any elements that aren't our spinner
		var frame_children = this.tableFrame.getChildren();
		frame_children.each(function(child, idx) { 
			if ( !child.hasClass('spinner') ) {
				child.dispose();
			}
		});
		
		if ( this.options.topPagination ) {
			this.generatePagination();
		}
		
		this.mooTable = new Element('table', {	'class':	'mooTable'	});
		this.mooTBody = new Element('tbody', {});
		
		this.generateHeaders();
		this.generateRows();
		
		// Add our table in
		this.mooTable.adopt(this.mooTBody);
		this.tableFrame.adopt(this.mooTable);
		
		// Add pagination
		this.generatePagination();
		
		if ( this.options.funcRenderCompleted ) {
			this.options.funcRenderCompleted();
		}
	},
	
	
	generateHeaders: function() {
		var row = new Element('tr', {'class':	'moo_table header'});
		var mootable = this;
		
		Object.each(this.headerItems, function(header, idx) {
			var tHeader = new Element('th', {	'class': 	'moo_data header' + ((header.sortable) ? ' sortable' : '') +
															((header.sortcode) ? ' '+header.sortcode : '') +
															((mootable.options.sortMode == header.sortcode) ? ' current-sort sort-'+mootable.options.sortDir : ''),
												'html':		header.text
										});
			
			if ( header.sortable ) {
				tHeader.addEvent('click', function() {	mootable.sortTable(header.sortcode);	});
			}
			
			row.adopt(tHeader);
		});
		
		this.mooTBody.adopt(row);
	},
	
	
	generateRows: function() {
		var mootable = this;
		
		// Work out our start/end indexes
		if ( this.options.updateType == 'replacement' ) {
			var startIdx = (this.options.currentPage - 1) * this.options.rowsPerPage;
			var endIdx = this.options.currentPage * this.options.rowsPerPage;
		} else {
			// If it's an ajax lookup it should be retrieving only the appropriate rows
			// from the system
			var startIdx = 0;
			var endIdx = this.options.rowsPerPage;
		}
		
		// If there's records, show them... otherwise show an error / not found message
		if ( mootable.options.totalResults > 0 ) {
			var rowNum = 0;
			
			Object.each(mootable.dataItems, function(item, idx) {
				if ( idx >= startIdx && idx < endIdx ) {
					var row = new Element('tr', {	'class': 'row'+(rowNum % mootable.options.zebraSize)});
					
					Object.each(mootable.headerItems, function(header, x) {
						row.adopt(new Element('td', {	'html': item[header.sortcode],
														'class': header.sortcode
													}));
					});
					
					// Run row creation function if any
					if ( mootable.options.funcRowCreate ) {
						mootable.options.funcRowCreate(row);
					}
					
					if ( mootable.options.funcRowOver ) {
						row.addEvent('mouseover', mootable.options.funcRowOver);
					}
					
					if ( mootable.options.funcRowOff ) {
						row.addEvent('mouseout', mootable.options.funcRowOff);
					}
					
					mootable.mooTBody.adopt(row);
					rowNum++;
				}
			});
		} else {
			var row = new Element('tr');
			
			row.adopt(new Element('td', {	'text': (mootable.errorMessage) ? mootable.errorMessage : 'No records to display',
											'colspan': mootable.columnCount,
											'class': 'no-records'
										}));
							
			mootable.mooTBody.adopt(row);
		}
	},
	
	
	generatePagination: function() {
		var pagination = new Element('div', {'class': 'pagination_frame'});
		var pages = (this.options.totalResults) ? Math.ceil(this.options.totalResults / this.options.rowsPerPage) : 1;
		var mootable = this;
		
		// First page link
		var navFirst = new Element((this.options.currentPage == 1) ? 'span' : 'a',	
											{	'href': 		void(0),
												'class':		'pagination nav_first',
												'text':			'«« first'
											});
		if ( this.options.currentPage != 1) {
			navFirst.addEvent('click', function() { 	mootable.firstPage();		});
		}
		
		
		// Prev page link
		var navPrev =  new Element((this.options.currentPage == 1) ? 'span' : 'a',	
											{	'href': 		void(0),
												'class':		'pagination nav_prev',
												'text':			'« prev'
											});
		if ( this.options.currentPage != 1) {
			navPrev.addEvent('click', function() { 	mootable.prevPage();		});
		}
		
		
		// Next page link
		var navNext =  new Element((this.options.currentPage == pages || !pages) ? 'span' : 'a',	
											{	'href': 		void(0),
												'class':		'pagination nav_next',
												'text':			'next »'
											});
		if ( this.options.currentPage != pages) {
			navNext.addEvent('click', function() {
				mootable.nextPage();
			});
		}
		
		// Last page link
		var navLast =  new Element((this.options.currentPage == pages || !pages) ? 'span' : 'a',
											{	'href': 		void(0),
												'class':		'pagination nav_last',
												'text':			'last »»'
											});
		if ( this.options.currentPage != pages) {
			navLast.addEvent('click', function() { 	mootable.lastPage();		});
		}

		
		// Page jump-to navigation
		var navPages =  new Element('div',	{	'href': 		void(0),
												'class':		'pagination nav_pages'
											});

		var pageMod = Math.ceil(pages / 10);
											
		if ( pages ) {		
			for ( x = 1; x <= pages; x++ ) {
				// Ok, we're showing a pagination link if:
				//    :: The number is within options.pageModClose of the current page, or the current page
				//    :: The number is the first page
				//    :: The number is the last page
				//    :: The number matches our chosen modulus
				//    :: The number of pages is less than options.pageModThreshold (display all)
				if ( (x >= (this.options.currentPage - this.options.pageModClose) && x <= (this.options.currentPage + this.options.pageModClose)) ||
					(x == 1) ||
					(x == pages) ||
					((x % this.options.pageMod) == 0) ||
					(pages <= this.options.pageModThreshold) ) {
						
					if ( x == this.options.currentPage ) {
						var pageLink =  new Element('span', {	'class':		'pagination nav_link_current',
																'text':			x
															});
					} else {
						var pageLink =  new Element('a', {	'href': 		void(0),
															'class':		'pagination nav_link',
															'text':			x,
															'pageNum':		x
														});
						pageLink.addEvent('click', function() {
							mootable.changePage(this.getProperty('pageNum'));
						});
					}
				
					navPages.adopt(pageLink);
				}
			}
		} else {
			var pageLink =  new Element('span', {	'class':		'pagination nav_link_current',
													'text':			' '
												});
			navPages.adopt(pageLink);
		}
		
		// Adopt everything
		pagination.adopt(navFirst, navPrev, navPages, navNext, navLast);
		
		this.tableFrame.adopt(pagination);
	},
	
	
					/*--------------------------------------------------------o
	----------------\                Sorting and Data Handling                |
		 sorting     \-------------------------------------------------------*/
					
	sortTable: function(column) {
		this.options.sortDir = (this.options.sortMode == column) ? ((this.options.sortDir == 'asc') ? 'desc' : 'asc') : 'asc';
		this.options.sortMode = column;
		
		// Sort with the current data if this isn't an ajax sort
		if ( this.options.updateType == 'replacement' ) {
			var mootable = this;
			
			this.dataItems = this.dataItems.sort(function(a, b) {
				if ( a[mootable.options.sortMode] == b[mootable.options.sortMode] ) {
					return 0;
				} else if ( a[mootable.options.sortMode] < b[mootable.options.sortMode] ) {
					return (mootable.options.sortDir == 'asc') ? -1 : 1;
				} else {
					return (mootable.options.sortDir == 'asc') ? 1 : 1;
				}
			});
	
			this.render();
		} else {
			// Reload our data with new sorting
			this.checkData();
		}
		
		return true;
	},
	
	
					/*--------------------------------------------------------o
	----------------\                    Ajax Data Handling                   |
		   ajax      \-------------------------------------------------------*/
	
	updateData: function() {
		var mootable = this;
		var data = null;
		
		mootable.errorMessage = null;
		
		if ( this.options.updateType == 'ajax-json' ) {
			var json_request = new Request.JSON({	'url':				this.options.requestUrl,
													'method':			'post',
													'data':				'page='+this.options.currentPage+
																		'&records_per_page='+this.options.rowsPerPage+
																		'&sort='+this.options.sortMode+
																		'&sort_dir='+this.options.sortDir,
													'onRequest':		function() {
																			if ( mootable.options.useSpinner && mootable.options.spinnerTarget ) {
																				mootable.createSpinner(mootable.options.spinnerTarget);
																			}
																		},
													'onComplete':		function() {
																			if ( mootable.options.useSpinner && mootable.options.spinnerTarget ) {
																				mootable.destroySpinner(mootable.options.spinnerTarget);
																			}
																		},
													'onSuccess':		function(data, raw) {
																			mootable.parseResponse(data);
																		},
													'onFailure':		function(response) {
																			mootable.errorMessage = 'Unable to retrieve data.';
																		}
												});
			json_request.send();
		} else {
			alert(this.options.updateType + ' not yet implemented!');
		}
		
	},
	
	
	parseResponse: function(data) {
		var mootable = this;
		
		// Ensure we have a valid response
		if ( data && data.summary && data.summary.totalResults &&
				data.headers && data.results ) {
			this.resetHeaders();
			this.resetItems();
			
			// Add headers
			Object.each(data.headers, function(header, idx) {
				mootable.addHeader(header.text, header.sortable, idx);
			});
			
			// Add data
			Object.each(data.results, function(result, idx) {
				mootable.addRow(result, idx);
			});
			
			mootable.options.totalResults = parseInt(data.summary.totalResults);
			
			if ( data.summary.sortMode ) {		mootable.options.sortMode = data.summary.sortMode;		}
			if ( data.summary.sortDir ) {		mootable.options.sortDir = data.summary.sortDir;		}
			
			if ( mootable.options.funcDataLoaded ) {
				mootable.options.funcDataLoaded(this);
			}
		} else {
			this.errorMessage = 'Malformed response received.';
		}
		
		this.render();
	},
	
	
	resetHeaders: function() {
		this.headerItems = {};
		this.columnCount = 0;
	},
	
	
	resetItems: function() {
		this.dataItems = Array();
		this.currentIndex = 0;
	},
	
	
	createSpinner: function(target) {
		var spinner = new Element('div', {	'class':	'spinner'	});
		
		if ( this.options.spinnerMessage ) {
			spinner.adopt(new Element('div', {	'class':	'spinner-msg',
												'text':		this.options.spinnerMessage
											}));
		}
		
		spinner.adopt(new Element('div', {	'class':	'spinner-img'	}));
		
		spinner.fx = new Fx.Morph(spinner, {	'duration':		this.options.spinnerFxTime,
												'transition':	this.options.spinnerFxTransition,
												'link':			'chain'
											});
		
		target.adopt(spinner);
		
		spinner.fx.start({	'opacity':	[0, 0.9] });
	},
	
	
	destroySpinner: function(target) {
		var spinner = target.getElement('.spinner');
		spinner.fx.start({	'opacity':	[0.9, 0] }).chain(function() {		spinner.dispose();	});
	},
	
					
					
					/*--------------------------------------------------------o
	----------------\                 Pagination and Navigation               |
		pagination   \-------------------------------------------------------*/
			
	changePage: function(page) {
		this.options.currentPage = parseInt(page);
		this.checkData();
	},
	
	
	firstPage: function() {
		this.options.currentPage = 1;
		this.checkData();
	},
	
	
	prevPage: function() {
		this.options.currentPage = (this.options.currentPage > 1) ? parseInt(this.options.currentPage) - 1 : 1;
		this.checkData();
	},
	
	
	nextPage: function() {
		this.options.currentPage = (this.options.currentPage < Math.ceil(this.options.totalResults / this.options.rowsPerPage)) ? parseInt(this.options.currentPage) + 1 : this.options.currentPage;
		this.checkData();
	},
	
	
	lastPage: function() {
		this.options.currentPage = Math.ceil(this.options.totalResults / this.options.rowsPerPage);
		this.checkData();
	}        
	
});
