//**************************************************************
//*	Class TetrisColumnizer
//**************************************************************
//*
//* Enable a newspaper column style of content positioning.
//* Column content will try to fill up empty (vertical)
//* space where possible (like Tetris but vertically flipped).
//* Column content can stretch out over multiple columns.
//* Column content must be absolutely postioned.
//*
//**************************************************************
//* Requires: DOM
//**************************************************************
//* Concepts:
//*
//*	A column-with-cells matrix is created. Empty cells are possible
//* just like in Tetris.
//*
//* Playing the 'Tetris' game will position content according to
//* this matrix and as high as possible, meaning filling the empty
//* space vertically where possible
//*
//**************************************************************
//* Objects:
//*		Column
//*			{width:integer,			//*** pixel width
//*			 offsetLeft:integer,	//*** pixelspace between this column and its parent object
//*									//*** automatically determined during creation
//*			 cells:array,			//*** it's cells
//*			 rowIndex:integer		//*** row index determines, which last row is filled with a cell
//*			 }
//*
//*		Cell (= actually a column row element)
//*			{id:string,					//*** content DOM object id
//*			 cumulativeHeight:integer,	//*** total column contentheight, this one included
//*			 colspan:integer;			//*** this cell is part of how many colspans
//*			 colspanned:boolean}		//*** true if this is one of the colspanned cells
//*
//**************************************************************
//* Created by sj5@stixeljockeys.com, 12-2005
//**************************************************************

function TetrisColumnizer(intXOffset, intYOffset, conExceptionType){
	//**************************************************************
	//*	Constructor
	//**************************************************************

	this.EVENT_END		= 0;//*** Event constant for the event 'end'. Called after column creation
	this.EXCEPTION_ALERT = "alert";
	this.EXCEPTION_STATUS = "status";
	this.EXCEPTION_HANDLING = "";

	this.intXOffset		= 0;//*** Where do columns begin to appear from the left
	this.intYOffset		= 0;//*** Where do columns begin to appear from the top
	this.intColumnCount;//*** Total number of columns Set when columniser receives the first cell
	this.intCellRowCount = 0;//*** Total number of cell rows. After cell-adding this value is the same for all columns, and used for looping through the matrix
	this.intColumnHeight = 0;//*** Total content height

	this.blnInited		= false;//*** When first column cell is added, Tetris is inited,
								//*** no more columns can be added
	this.blnPlayed		= false;//*** When play functions is called. No more columns or cells can be added
	this.arrColumns		= new Array();//*** Holds the columninfo as well as the added cellcontent
	this.arrListeners	= new Array();//*** Holds eventlistener functions

	if(typeof(intXOffset) != "undefined"){
		if(!isNaN(intXOffset)){this.intXOffset = intXOffset;}
	}
	if(typeof(intYOffset) != "undefined"){
		if(!isNaN(intYOffset)){this.intYOffset = intYOffset;}
	}
	if(typeof(conExceptionType) != "undefined"){
		this.EXCEPTION_HANDLING = conExceptionType;
	}
}

TetrisColumnizer.prototype.throwException = function(strError){
	switch(this.EXCEPTION_HANDLING){
		case "alert":alert(strError);break;
		case "status":window.status = strError;break;
		default:break;
	}
};

TetrisColumnizer.prototype.addColumn = function(intWidth, intMarginLeft){
	//**************************************************************
	//*	Goal: adds columns
	//* Parameters:
	//*		intWidth (integer). Width of the column in pixels
	//*		[intMarginLeft] (integer). If defined, determines the
	//*		the space in pixels between this column's position and
	//*		the previous one. Default = 0;
	//**************************************************************

	if(!this.blnInited && !this.blnPlayed){
		if(typeof(intMarginLeft) == "undefined"){intMarginLeft = 0;}
		if(this.arrColumns.length > 0){
			var objColumn = this.getColumn(this.arrColumns.length);
			intMarginLeft += objColumn["offsetLeft"] + objColumn["width"];
		}else{
			intMarginLeft += this.intXOffset;
		}
		this.arrColumns[this.arrColumns.length] = {width:intWidth,
												   offsetLeft:intMarginLeft,
												   cells:new Array(),
												   rowIndex:-1};
	}else{
		this.throwException("TetrisColumnizer.addColumn - Already inited or created. No columns can be added anymore.");
	}
};

TetrisColumnizer.prototype.getColumn = function(intColumn){
	//*** get column object at column intColumn
	if(typeof(this.arrColumns[intColumn-1]) != "undefined"){
		return this.arrColumns[intColumn-1];
	}return false;
};

TetrisColumnizer.prototype.setColumn = function(intColumn, objColumn){
	//*** replace column object at intColumn with objColumn
	if(typeof(this.arrColumns[intColumn-1]) != "undefined"){
		this.arrColumns[intColumn-1] = objColumn;
	}
};

TetrisColumnizer.prototype.addCell = function(strObjectId, intColumn, intColspan){
	//**************************************************************
	//*	Goal: adds column cells
	//* Parameters:
	//*		strObjectId (String). To get the content object by Id and
	//*			be able to change it's parameters
	//*		intColumn (integer). Which column to add the content to
	//*		[intColspan] (integer). If defined, determines how many columns this content
	//*			element occupies. Default = 1;
	//**************************************************************

	if(!this.blnPlayed){

		if(!this.blnInited){
			this.blnInited = true;
			this.intColumnCount = this.arrColumns.length;
		}

		var intColspanReal = 1;
		if(typeof(intColspan) != "undefined"){if(!isNaN(intColspan)){intColspanReal = intColspan;}}

		if((intColspanReal-1)+intColumn <= this.arrColumns.length){

			var objColumn = this.getColumn(intColumn);
			if(objColumn){

					if(intColspanReal == 1){
						//*** add cell to column, save raised index to column
						var intRowIndex = objColumn["rowIndex"];
						intRowIndex++;

						objColumn["cells"][intRowIndex] = {id:strObjectId,
															cumulativeHeight:0,
															colspan:1,
															colspanned:false};

						objColumn["rowIndex"] = intRowIndex;
						this.setColumn(intColumn, objColumn);

						//*** raise total number of rows if necessary
						if(intRowIndex+1 > this.intCellRowCount){this.intCellRowCount = intRowIndex+1};

					}else{

						/***
							if any of the colspanned columns has the intRowIndex position
							taken, find the next highest possible intRowIndex through each colspanned column
							 Fill each colspanned column with empty cells untill intRowIndex is reached
						***/
						var intNewHighestRowIndex = this.getHighestRowIndex(intColumn, intColspan);

						//*** add cell to column at this new highest index and save adjustments to the column
						objColumn = this.getColumn(intColumn);//*** be sure to get the new values
						objColumn["cells"][intNewHighestRowIndex] = {id:strObjectId,
															cumulativeHeight:0,
															colspan:intColspanReal,
															colspanned:false};

						objColumn["rowIndex"] = intNewHighestRowIndex;
						this.setColumn(intColumn, objColumn);

						//*** save adjustments to each colspanned column as well
						for(var c=intColumn+1;c<intColumn+intColspan;c++){
							var cspanColumn = this.getColumn(c);

							cspanColumn["cells"][intNewHighestRowIndex] = {id:strObjectId,
																cumulativeHeight:0,
																colspan:intColspanReal,
																colspanned:true};

							cspanColumn["rowIndex"] = intNewHighestRowIndex;
							this.setColumn(c, cspanColumn);
						}

						//*** raise total number of rows if necessary
						if(intNewHighestRowIndex+1 > this.intCellRowCount){this.intCellRowCount = intNewHighestRowIndex+1};
					}
			}
		}else{
			this.throwException("TetrisColumnizer.addCell - colspan too large");
		}
	}else{
		this.throwException("TetrisColumnizer.addCell - TetrisColumnizer already played.");
	}
};

TetrisColumnizer.prototype.getHighestRowIndex = function(intColumn, intColspan){
	//**************************************************************
	//*	Goal: returns the next highest column rowIndex for affected columns.
	//*			each affected column is filled with empty cells where necessary.
	//*			the function is only used for multi column spans
	//* Parameters:
	//*		intColumn (integer). To which column is the content added
	//*		intColspan (integer). If defined, determines how many columns this content
	//*			element occupies.
	//* Returns: integer
	//**************************************************************

	var arrColumns = new Array();
	var arrIndices = new Array();

	for(var c=intColumn;c<intColumn+intColspan;c++){
		arrColumns[""+c] = this.getColumn(c);
		arrIndices[arrIndices.length] = arrColumns[""+c]["rowIndex"];
	}

	arrIndices.sort(new Function("a", "b", "return b-a;"));//*** sort with highest number lowest index
	var intHighest = arrIndices[0];

	//*** fill columns with empty cells untill index intHighest is reached
	var objEmptyCell = {id:-1,
						cumulativeHeight:0,
						colspan:1,
						colspanned:false};

	for(var cs in arrColumns){
		var objColumn = arrColumns[cs];
		arrCells = objColumn["cells"];
		if(arrCells.length <= intHighest){
			while(arrCells.length <= intHighest){
				arrCells[arrCells.length] = objEmptyCell;
			}
			objColumn["cells"] = arrCells;
			objColumn["rowIndex"] = intHighest;
			//*** save column changes
			this.setColumn(parseInt(cs), objColumn);
		}
	}

	//*** return the index+1, this will become the new highest index
	return(intHighest+1);
};

TetrisColumnizer.prototype.play = function(){
	//**************************************************************
	//*	Goal: creates columns according to the added columns and cells matrix
	//**************************************************************
	var cr=0;
	var crLength = this.intCellRowCount;
	var cmLength = this.arrColumns.length;

	while(cr<crLength){

		var cm=0;
		while(cm < cmLength){

			var objColumn 	= this.getColumn(cm+1);
			var objCell = objColumn["cells"][cr];

			if(typeof(objCell) != "undefined"){
				if(objCell["id"] != -1){

					if(objCell["colspan"] == 1){

						var intYOffset;
						if(cr == 0){
							intYOffset = this.intYOffset;
						}else{
							intYOffset = objColumn["cells"][cr-1]["cumulativeHeight"];
						}

						//*** position content object
						var objContent = document.getElementById(objCell["id"]);
						this.setObjectDOMProperty(objContent, "left", objColumn["offsetLeft"]);
						this.setObjectDOMProperty(objContent, "top", intYOffset);
						this.setObjectDOMProperty(objContent, "width", objColumn["width"]);
						this.setObjectDOMProperty(objContent, "visibility", "visible");

						objCell["cumulativeHeight"] = intYOffset + this.getObjectDOMProperty(objContent, "height");

						//*** save adjustments
						objColumn["cells"][cr] = objCell;

					}else{

						if(!objCell["colspanned"]){

							var intWidthSpan = 0;
							var intHighest;
							var intHeight;
							var objContent
							var arrColumns = new Array(objColumn);
							var arrHeights = new Array();

							if(cr>0){
								arrHeights[arrHeights.length] = objColumn["cells"][cr-1]["cumulativeHeight"];
							}else{
								arrHeights[arrHeights.length] = this.intYOffset;
							}

							//*** determine for each affected column cell which one has the highest previous cumulativeHeight.
							for(var c=cm+2;c<=cm+objCell["colspan"];c++){
								arrColumns[""+c] = this.getColumn(c);
								if(cr>0){
									arrHeights[arrHeights.length] = arrColumns[""+c]["cells"][cr-1]["cumulativeHeight"];
								}else{
									arrHeights[arrHeights.length] = this.intYOffset;
								}
								//*** save last cell's offsetleft+width for determining colspanned cellwidth
								if(c==cm+objCell["colspan"]){
									intWidthSpan = arrColumns[""+c]["offsetLeft"] + arrColumns[""+c]["width"];
								}
							}

							arrHeights.sort(new Function("a", "b", "return b-a;"));//*** sort with highest number lowest index

							intHighest = arrHeights[0];

							objContent = document.getElementById(objCell["id"]);
							this.setObjectDOMProperty(objContent, "left", objColumn["offsetLeft"]);
							this.setObjectDOMProperty(objContent, "top", intHighest);
							this.setObjectDOMProperty(objContent, "width", intWidthSpan - objColumn["offsetLeft"]);//*** content object's width should now be adjusted to the saved columnwidth minus offsetLeft of this cell
							this.setObjectDOMProperty(objContent, "visibility", "visible");

							objCell["cumulativeHeight"] = intHighest + this.getObjectDOMProperty(objContent, "height");

							//*** save adjustments
							objColumn["cells"][cr] = objCell;

							//*** save new cumulative height in colspanned columns
							for(var cs in arrColumns){
								var objColumnL = arrColumns[cs];
								objColumnL["cells"][cr]["cumulativeHeight"] = objCell["cumulativeHeight"];
								this.setColumn(parseInt(cs), objColumnL);
							}
						}
					}

				}else{
					//*** repeat cumulativeHeight of the previous cell
					if(cr>0){
						objColumn["cells"][cr]["cumulativeHeight"] = objColumn["cells"][cr-1]["cumulativeHeight"];
					}else{
						objColumn["cells"][cr]["cumulativeHeight"] = this.intYOffset;
					}
				}

				if(objColumn["cells"][cr]["cumulativeHeight"] > this.intColumnHeight){this.intColumnHeight = objColumn["cells"][cr]["cumulativeHeight"];}

				//*** save adjustments
				this.setColumn(cm+1, objColumn);

			}else{
				//*** do nothing, unused cell
			}

			cm++;
		}
		cr++;
	}
	this.throwEvent(this.EVENT_END);
};


TetrisColumnizer.prototype.getObjectDOMProperty = function(obj, strProperty){
	switch(strProperty){

		case "height":
			return obj.offsetHeight;
			break;

		case "width":break;
	}
	return false;
};

TetrisColumnizer.prototype.setObjectDOMProperty = function(obj, strProperty, objValue){
	switch(strProperty){
		case "left":
			obj.style.left = objValue +"px";
			break;

		case "top":
			obj.style.top = objValue +"px";
			break;

		case "width":
			obj.style.width = objValue +"px";
			break;

		case "visibility":
			obj.style.visibility = objValue;
			break;
	}
};


TetrisColumnizer.prototype.addEventListener = function(strFuncListener, intEvent){
	//**************************************************************
	//*	Goal: adds listener functions to the event array
	//* Parameters:
	//*		strFuncListener (String). the function to call on self via self.apply[strFunctionName]
	//*		intEvent (integer/constant). register for which event
	//**************************************************************
	if(typeof(strFuncListener) == "string" && typeof(self[strFuncListener]) != "undefined"){
		var arrEvent;
		if(typeof(this.arrListeners[""+intEvent]) == "undefined"){this.arrListeners[""+intEvent] = new Array();}
		arrEvent = this.arrListeners[""+intEvent];
		arrEvent[arrEvent.length] = strFuncListener;
	}
};

TetrisColumnizer.prototype.throwEvent = function(intEvent){
	//**************************************************************
	//*	Goal: executes listener functions
	//* Parameters:
	//*		intEvent (integer/constant). execute listener functions for which event
	//**************************************************************
	var arrEvent;
	if(typeof(this.arrListeners[""+intEvent]) != "undefined"){
		arrEvent = this.arrListeners[""+intEvent];
		for(var j=0;j<arrEvent.length;j++){
			if(typeof(self[arrEvent[j]]) != "undefined"){

				var arrArgs = new Array();
				switch(intEvent){
					case this.EVENT_END:
						arrArgs[arrArgs.length] = this.intColumnHeight;
						break;
				}
				self[arrEvent[j]].apply(self, arrArgs);
			}
		}
	}
};