//////////////////////////////////////////////////////////////////
//																//
//					user overrideable classes					//
//																//
//////////////////////////////////////////////////////////////////
/*
ElasticTable accept a bindObjectEvents parameter. bindObjectEvents contains event handlers
to be attached to each cell (ie td & th) within the elasticTable. ElasticTableEvents
are the default events if no bindObjectEvents object is supplied. Additonally the
events can be bound to a different object (eg a <a> within the cell) by
overriding the getBindObject function.
*/
var ElasticTableEvents = {
	'click'	: function(evt, elasticTable){
		var event = new Event(evt);
		var cell=null;
		var pNode=$(event.target);
		var selectedSetters=null;

		while (pNode.getTag()!='td' && pNode.getTag()!='th' && pNode!=elasticTable.options.table){
			pNode=pNode.getParent();
		}
		cell=pNode;

		if (cell==elasticTable.options.table || cell==null) return null;
		if (cell.hasClass('nonselectable')) return null;
		
		elasticTable.animation.stop();

		if (elasticTable.clearSelected()!=cell) elasticTable.markSelected(cell);

		selectedSetters=elasticTable.animation.getSelectedSetters();

		elasticTable.animation.generateAnimation(selectedSetters);
		elasticTable.animation.start(500);
	}
};




var ElasticCellEvents = {
}

var getCellEventsBindObject = function(cell){
	var result=cell.parentNode
	
	if (result['href']) result.href="#";			
	
	return result;
}


//////////////////////////////////////////////////////////////////
//																//
//						elasticTable class						//
//																//
//////////////////////////////////////////////////////////////////
var ElasticTable = new Class({	
	options: {
			table: null,
			ElasticTableEvents: ElasticTableEvents,
			ElasticCellEvents: ElasticCellEvents,
			getBindObject: getCellEventsBindObject,
			minWidth: '4%',
			minHeight: '4%'
	},
	
	
	
	initialize: function(options){
		var elasticTable=this;
		this.setOptions(options);
		this.options.table=$(this.options.table);

		if (!this.options.table) return null;
			
		//add events to table
		$each(this.options.ElasticTableEvents, function(value, key){
			var boundFunction=value.create({
				bind: elasticTable.options.table,
				event: true,
				arguments: [elasticTable]
			});
			elasticTable.options.table.addEvent(key, boundFunction);
		});	
		
		
		//add liner and events to cell
		for (var r=0; r<this.options.table.rows.length; r++) {
			for (var c=0; c<this.options.table.rows[r].cells.length; c++) {
				this.addLinerAndEventsToCell(this.options.table.rows[r].cells[c]);
			}
		}

		//if 'heightsetter' is not pre-specified add to first row
		if (!this.filterToTableChildren($$('.heightsetter'))) {
			var r=0;
			while (r<this.options.table.rows.length) {
				this.options.table.rows[r].cells[0].addClass('heightsetter');
				r+=this.options.table.rows[r].cells[0].rowSpan || 1;
			}
		}

		//if 'widthsetter' is not pre-specified add to first row
		if (!this.filterToTableChildren($$('.widthsetter'))){
			for (var c=0; c<this.options.table.rows[0].cells.length; c++) {
				this.options.table.rows[0].cells[c].addClass('widthsetter');
			}
		}		

		//set up animation object
		this.animation=new this.cellAnimator(this);
		this.animation.generateAnimation(this.animation.getSelectedSetters());
		this.animation.start(0);
	},
	
	
	
	addLinerAndEventsToCell: function(cell){
		var elasticTable=this;
		var liner=new Element('div')

		//move the content of the cell into liner so cell content doesn't affect the table dimensions		
		liner.addClass('elasticliner')
		while (cell.childNodes[0]) liner.appendChild(cell.childNodes[0]);
		liner.injectInside(cell);
		
		//add the events to the cell
		$each(this.options.ElasticCellEvents, function(value, key){
			var bindObject=elasticTable.options.getCellEventsBindObject(cell);
			var boundFunction=value.create({
				bind: bindObject,
				event: true,
				arguments: [cell, elasticTable]
			});
			bindObject.addEvent(key, boundFunction);
		});	
	},
	
	

	getSelected: function(){
		var selection=this.filterToTableChildren($$('.selected'));

		if (selection==null) return null;
		
		return selection[0];
	},


	
	markSelected: function(element){
		var toMark=$(element);

		if (!toMark) return null;

		toMark.addClass('selected');
		return true;
	},
	
	
	
	clearSelected: function(){
		var oldSelection=this.getSelected();

		if (oldSelection==null) return null;

		oldSelection.removeClass('selected');
		return oldSelection;
	},
	
	
	
	filterToTableChildren: function(input){
		var result=$A([]);
		var parent=null;

		if (input==null) return null;

		if (input[length]==null) {//not an array
			parent=input.parentNode;
			while (parent!=null) {
				if (parent==this.options.table) return input;
				parent=parent.parentNode;

			}	
			return null;
		}

		for (var i=0; i<input.length; i++){
			parent=input[i].parentNode;
			while (parent!=null) {
				if (parent==this.options.table) {
					result.push(input[i]);
					parent=null;
				} else {
					parent=parent.parentNode;
				}
			}
		}
		if (result.length==0) return null;
		return result;
	},
  
	
	
	
	/*----------------------------------------------------------*/
	/*							animation						*/
	/*----------------------------------------------------------*/
	cellAnimator: new Class({
		initialize: function(elasticTable){
			this.elasticTable=elasticTable;
			this.clear();
		},
		
		
		getSelectedSetters: function(){
			var rowLength=0;
			var colLength=0;
			var selectedRowIndex=null;
			var selectedColIndex=null;
			var selected=this.elasticTable.getSelected();
			var table=this.elasticTable.options.table;
			var result={rows: $A([]), columns: $A([])};

			if (!selected) return result;

			//get length of rows and cols						
			for (var i=0; i<table.rows.length; i++)				rowLength+=table.rows[i].cells[0].rowSpan || 1;
			for (var i=0; i<table.rows[0].cells.length; i++)	colLength+=table.rows[0].cells[i].colSpan || 1;
				
			//create refGrid
			var refGrid=new Array(rowLength);
			for (var r=0; r<refGrid.length; r++){
				refGrid[r]=new Array(colLength);
				for (var c=0; c<refGrid[r].length; c++){
					refGrid[r][c]=null;
				}
			}
			
			//cycle through cells until the current cell is found and the related index of the heightsetter and widthsetter can be determined
			for (var r=0; r<table.rows.length; r++) {
				for (var c=0; c<table.rows[r].cells.length; c++) {
					var cell=table.rows[r].cells[c];

					//find next free space in refGrid
					var refR=0;
					var refC=0;
					while (!$A(refGrid[refR]).contains(null)) refR++;
					while (refGrid[refR][refC]!=null) refC++;

					if (cell==selected) {
						selectedRowIndex=refR;
						selectedColIndex=refC;
					}
					
					//fill refGrid with cell
					var rowSpan=cell.rowSpan || 1;
					var colSpan=cell.colSpan || 1;
					for (var rs=0; rs<rowSpan; rs++){
						for (var cs=0; cs<colSpan; cs++){
							refGrid[refR+rs][refC+cs]=cell;
						}
					}
				}
				
				//if we have all information to determine the setters then determine them and exit
				if ((selectedRowIndex!=null && refGrid[selectedRowIndex + (selected.rowSpan || 1)-1][0]!=null) && 
					(selectedColIndex!=null && refGrid[0][selectedColIndex + (selected.colSpan || 1)-1]!=null)) {
//could this be done by counting through the widthSetter & heightSetter arrays? Yes, providing the arrays are ordered.
						for (var rs=0; rs<(selected.rowSpan || 1); rs++) result.rows.push(refGrid[selectedRowIndex+rs][0]);
						for (var cs=0; cs<(selected.colSpan || 1); cs++) result.columns.push(refGrid[0][selectedColIndex+cs]);
						return result; 
				}
			}
			
			return result;	//this return will always return empty arrays
		},			
						
			
			
		generateAnimation: function(selectedSetters){
			this.generateDimAni(this.elasticTable.filterToTableChildren($$('.heightsetter')), selectedSetters.rows, 'fixedheight', 'height', this.elasticTable.options.minHeight, 'rowSpan', this.elasticTable.options.table);
			this.generateDimAni(this.elasticTable.filterToTableChildren($$('.widthsetter')), selectedSetters.columns, 'fixedwidth', 'width', this.elasticTable.options.minWidth, 'colSpan', this.elasticTable.options.table);
		},				
		
		
		generateDimAni: function(allSetters, selectedSetters, fixedClass, dimension, minDim, span, table){
			var remainingDimAlloc=100;
			var dimInPX=table.getStylePx(dimension).toInt();

			//set fixed setters
			for (var i=0; i<allSetters.length; i++){
				if (allSetters[i].hasClass(fixedClass)) {
					var newValue=8; //arbitary default of 8%
					var unit=getUnit(allSetters[i].getStyle(dimension));

					if (unit=='px'){
						newValue=(allSetters[i].getStyle(dimension).toInt() / dimInPX)*100;
					} else if (unit=='%'){
						newValue=allSetters[i].getStyle(dimension).toFloat();
					}

					remainingDimAlloc-=newValue;
				}
			}

			//remove fixed from allSetters and selectedSetters
			allSetters=removeElementsWithClass(allSetters, fixedClass);
			if (selectedSetters) selectedSetters=removeElementsWithClass(selectedSetters, fixedClass);

			
			if (selectedSetters.length>0) {//there is a selected cell
				var selectedDimAlloc=0;

				//set unselected items to minDim
				for (var i=0; i<allSetters.length; i++) {
					if (!selectedSetters.contains(allSetters[i])) {
						var newValue=minDim.toFloat() * (allSetters[i][span] || 1);//assume minDim is a %
						var unit=getUnit(minDim);

						if (unit=='px')	newValue=(minDim.toInt() / dimInPX) * 100 * (allSetters[i][span] || 1);
						remainingDimAlloc-=newValue;
						this.addObject(allSetters[i], this.compileToObject(dimension, newValue));
					}
				}
				
				//set selected col widths	
				selectedDimAlloc=remainingDimAlloc / selectedSetters.length-1;
				for (var i=0; i<selectedSetters.length; i++){
					this.addObject(selectedSetters[i], this.compileToObject(dimension, selectedDimAlloc), true);
				}


			} else {//there are no selected cells so each dim gets distributed evenly
				var setterCount=0;
				var evenDimAlloc=0;				
	
				for (var i=0; i<allSetters.length; i++) setterCount+=allSetters[i][span] || 1;
				evenDimAlloc=remainingDimAlloc / setterCount;

				for (var i=0; i<allSetters.length; i++){
					if (!allSetters[i].hasClass(fixedClass)){
						this.addObject(allSetters[i], this.compileToObject(dimension, evenDimAlloc * (allSetters[i][span] || 1)));
					}	
				}
				
			}
		},
		
	
	
		addObject: function(element, options, amend){
			var index=this.elements.indexOf(element);
			if (index==-1){
				this.elements.push(element);
				this.options[this.elements.length-1]=options;
			} else {
				if (amend) {
					//add the value of each parameter options to the value currently in this.options
					this.options[index]=$merge(this.options[index], options);
				} else {
					this.options[index]=$merge(this.options[index], options);
				}
			}
		},
		
		
		
		compileToObject: function(parameter, value){
			var result={};
			result[parameter]=value;
			return result;		
		},
		
		
				
		clear: function(){
			this.elements=$A([]);
			this.options={};
		},
		
		
		
		start: function(duration){
			this.animation = new Fx.Elements(this.elements, {duration: duration, unit: '%'});
			this.animation.start(this.options);	
		},
		
		
		
		stop: function(){
			this.animation.stop();
		}
	})
});


ElasticTable.implement(new Options);

//////////////////////////////////////////////////////////////////
//																//
//						Helper functions						//
//																//
//////////////////////////////////////////////////////////////////
Element.extend({
  /**
   * Calculates the absolute measurement of the style e.g. "pixels".
   * Fails on % values, original value is returned instead.
   * @param {String} property : CSS property to be read
   */
	getStylePx: function(property){
		var PX = /^\d+(px)?$/i;
    	var value = this.getStyle(property);
		if (PX.test(value)) {return parseInt(value);}
		if (value.indexOf("%") > -1) {return value;}
		var style = this.style.left;
		var runtimeStyle = this.runtimeStyle.left;
		this.runtimeStyle.left = this.currentStyle.left;
		this.style.left = value || 0;
		value = this.style.pixelLeft;
		this.style.left = style;
		this.runtimeStyle.left = runtimeStyle;
		return value;
	}
});

	
function sortElementsByID(arg1, arg2){
	if (arg1.id<arg2.id) return -1;
	if (arg1.id==arg2.id) return 0;
	if (arg1.id>arg2.id) return +1;	
}	


function removeElementsWithClass(elements, className){
	var result=$A([]);
	
	for (var i=0; i<elements.length; i++){
		if (!elements[i].hasClass(className)){
			result.push(elements[i]);
		}
	}

	return result;
}


function getUnit(valueUnitString){
	return valueUnitString.replace(/\d+[.]?\d*/, '');
}
