
/*
 * Represents the grid object. It holds the cells and
 * provides functionality to manage the cells.
 */
function Grid(){

	//The last occupied column. Used to iterate through the grid cells.
	this.lastCol = 1;
	//The last occupied row. Used to iterate through the grid cells.
	this.lastRow = 1;
	//Maximum row count. Default is 1, has to be updated by client.
	this.colsCount = 1;
	//Row Array. Each element in this array is an Array of columns.
	this.rows = new Array();

	//Top left corner of the cells container.
	this.left = 0;
	this.top = 0;

	//Grid height. Used to update the cells container height so other
	//elements on the page draw correctly. 
	this.height = 0;

	//Array used to store the next available vertical position.
	//Used and updated when cells are positioned.
	this.nextPositionInCol = new Array();
	//Id of the main HTML element containing the cells.
	this.cells_container_id = '';
	//Vertical and horizontal separation between cells.
	this.separation_size = 10;
	//Default column width.
	this.col_width = 190;
	//Array containing the width of each column. Used when columns must have different widths.
	this.colsWidths = new Array();
	
	//This is the grid name
	this.name = '';
	
	//Adds a cell to the grid. Updates lastCol and lastRow attributes.
	this.addCell = function(col, row, cell){
		if (this.rows[row] == null){
			this.rows[row] = new Array();
		}
		this.rows[row][col] = cell;
		if (row > this.lastRow){
			this.lastRow = row;
		}
		if ((col + cell.cols - 1) > this.lastCol){
			this.lastCol = (col + cell.cols - 1);
		}
		
		cell.col = col;
		cell.row = row;
		
		//Set the grid object for the HTML element represented by the cell so it can be used in drag&drop callbacks.
		$(cell.div_id).gridObject = this;
		
	};
	
	//returns the last row number
	this.updateRowCount = function() {
		var current = this.lastRow;
		for (var i = this.lastRow; i >= 1; i--) {
			if (this.isNullRow(i) == true) {
				current = i - 1;
			} else {
				break;
			}
		}
		return current;
	}
	
	//checks if the given row is empty (it has only null cells)
	this.isNullRow = function(row) {
		var ret = true;
		for(var j = 1; j <= this.lastCol; j++) {
			var cell = this.getParentCell(j, row);
			if (cell != null) {
				return false;
			}
		}
		return ret;
	}
	
	//returns the cell in the given col and row, null if it does not exists
	this.getCell = function(col, row){
		if (this.rows[row] == null)
			return null;
		return this.rows[row][col];
	};

	//Make each of the grid cells visible.
	this.displayCells = function(){
		for(var row=1; row <= this.lastRow; row++){
			for(var col=1; col <= this.lastCol; col++){
				var cell = this.getCell(col, row);
				if (cell != null){
					if (cell.show()){
						var cell_div = document.getElementById(cell.div_id);
						cell_div.style.display = 'inline';
					}
				}
			}
		}
	}
	
	//Positions the cells on the page using thir real visible height. It also updates the cells container height.
	this.positionCells = function(){
		this.updateContainerDimensions(); //init container dimensions so positioning can be done correctly

		//get left and top position of the grid container so cells can be positioned relative to it
		this.top = getElementTop(this.cells_container_id);
		this.left = getElementLeft(this.cells_container_id);
		this.height = 0;
		
		for (var col=1; col <= this.lastCol; col++){
			this.nextPositionInCol[col] = this.top;
		}
		
		for(var row=1; row <= this.lastRow; row++){
			for(var col=1; col <= this.lastCol; col++){
				var cell = this.getCell(col, row);
				if (cell != null){
					if (cell.show()){
						var leftPos = this.getLeftPositionForCol(col);
						var maxPos = this.getMaxPositionForCellIn(cell, col);
						cell.setPosition(leftPos, maxPos);
						this.updateMaxPositionsUsing(cell, col);
					}
				}
			}
		}
	
		this.updateContainerDimensions(); //update container dimensions after positioning
		this.lastRow = this.updateRowCount();
	}

	
	//Returns the vertical position where the given cell, in the given column, should be positioned.
	this.getMaxPositionForCellIn = function(cell, startCol){
		var max = 0;
		for(var col=startCol; col < startCol + cell.cols; col++){
			if (this.nextPositionInCol[col] > max){
				max = this.nextPositionInCol[col];
			}
		} 
		return max;
	}
	
	//Updates the 'nextPositionInCol' Array using the given cell height and the start column.
	//If the cell column count is > 1, more than 1 element in the 'nextPositionInCol' Array will be updated.
	this.updateMaxPositionsUsing = function(cell, startCol){
		for(var col=startCol; col < startCol + cell.cols; col++){
			this.nextPositionInCol[col] = cell.y + cell.getHeight();
			if (this.nextPositionInCol[col] - this.top > this.height){
				this.height = this.nextPositionInCol[col] - this.top;
			}
			this.nextPositionInCol[col] = this.nextPositionInCol[col] + this.separation_size;
		}
	}
	
	//Updates the cells container height and width so all cells can fit in it.
	this.updateContainerDimensions = function(){
		var cells_container_element = document.getElementById(this.cells_container_id);
		cells_container_element.style.height = this.height + 'px';
		cells_container_element.style.width = this.getLeftPositionForCol(this.colsCount) + this.getColWidth(this.colsCount) - this.left;
	}
	
	//Returns the horizontal position (left end) of the column.
	this.getLeftPositionForCol = function(col){
		var retVal = this.left;
		for (var i = 1; i < col; i++){
			retVal += this.getColWidth(i) + this.separation_size;
		}
		return retVal;
	}
	
	//Returns the width of the given column. If no width for the column was specified, it returns the default width.
	this.getColWidth = function(col){
		if(this.colsWidths[col] == null){
			return this.col_width;
		}
		return this.colsWidths[col];
	}
	
	//Pushes down the cell in the given position, pushing down the cells below it too.
	this.pushCellDown = function(cell){
		if (cell == null){
			return;
		}

		//first push down all cell bellow this one
		var cellsBelow = this.getCellsBelow(cell);
		cellsBelow.each(function(c){this.pushCellDown(c);}, this);

		//move the cell down (the empty space left will be removed when grid pushUp is called).
		this.removeCell(cell);	
		this.addCell(cell.col, cell.row + 1, cell);
	}

	//Returns an array with the cells inmediatelly below the given cell. (because a cell can have
	//more than 1 column, it can have more than 1 cell bellow it)
	this.getCellsBelow = function(cell){
		var ret = new Array();
		var arrayPos = 0;
		for(var i=cell.col; i < cell.col + cell.cols; i++){
			var nextCell = this.getParentCell(i, cell.row + 1);
			if (!ret.test(nextCell)){
				ret[arrayPos++] = nextCell;
			}
		}
		return ret;
	}

	//moves up all cells that have empty spaces above them
	this.pushUp = function(){
		var allCells = this.getAllCells();
		allCells.each(function(cell){this.pushCellUp(cell);},this);
	}
	
	//Moves the cell vertically so there is no empty spaces above it.
	this.pushCellUp = function (cell) {
		//get the cell new row
		var newRow = this.getFreeSpaceAboveForCell(cell);
		
		//move the cell to its new row
		if (newRow != cell.row){
			this.removeCell(cell);
			this.addCell(cell.col, newRow, cell);
		}
	}

	//returns the first free row upwards where the given cell can be located
	this.getFreeSpaceAboveForCell = function(cell){
		cell = this.getParentCell(cell.col, cell.row);
		var nextCell = null;
		for (var row = cell.row-1; row > 0; row--){
			for (var col = cell.col; col <= (cell.col + cell.cols - 1); col++){
				nextCell = this.getParentCell(col, row);
				if (nextCell != null){
					return nextCell.row + 1;
				}
			}
		}
		return 1;
	}
	
	//returns the row below the first cell above the given position
	this.getFreeRowAbovePos = function(col, row){
		var nextCell = null;
		for (var r = row-1; r > 0; r--){
				nextCell = this.getParentCell(col, r);
				if (nextCell != null){
					return nextCell.row + 1;
				}
			}
		
		return 1;
	}
	
	//Moves the given cell to the given column and row, making room for the cell at the new position.
	this.moveCellTo = function(col, row, cell){
		changeSaveButtonText();
		this.makeRoomForCellIn(cell, col, row);
		this.removeCell(cell);
		this.addCell(col, row, cell);
	}

	//pushes down the necesary cells so the given cell can be located in the given col and row
	this.makeRoomForCellIn = function(cell, col, row){
		for(var i=col; i <= (col + cell.cols - 1); i++){
			this.pushCellDown(this.getParentCell(i, row));
		}
	}

	//swaps two cells (places each of them in the place of the other)
	this.swapCells = function(cell1, cell2){
		changeSaveButtonText();
		this.removeCell(cell1);
		this.removeCell(cell2);
		
		var higherCell = (cell1.row <= cell2.row)?cell1:cell2;
		var lowerCell = (cell1.row <= cell2.row)?cell2:cell1;
		
		var higherPos = { col: higherCell.col, row: higherCell.row };
		var lowerPos = { col: lowerCell.col, row: lowerCell.row };
		
		this.makeRoomForCellIn(higherCell, lowerPos.col, lowerPos.row);
		this.addCell(lowerPos.col, lowerPos.row, higherCell);
		this.makeRoomForCellIn(lowerCell, higherPos.col, higherPos.row);
		this.addCell(higherPos.col, higherPos.row, lowerCell);
	}
	
	//Removes the given cell from the grid.
	this.removeCell = function(cell){
		if (this.rows[cell.row] == null){
			return;
		}
		this.rows[cell.row][cell.col] = null;
	}
	
	//Return the parent cell of the cell in the given column and row.
	//A parent cell is the a cell that contains others because it occupies more than 1 column.
	this.getParentCell = function(col, row){
		for(var i=col; i > 0; i--){
			var cell = this.getCell(i, row);
			if (cell != null && (cell.col + cell.cols - 1) >= col){
				return cell;
			}
		}
		return null;
	}
	
	//returns all the cells in the grid. This does not includes 'child cells' (cells that have a parent cell) and
	//does includes blank cells (used for drag&drop)
	this.getAllCells = function(){
		var ret = new Array();
		var arrayPos = 0;
		for(var row=1; row <= this.lastRow; row++){
			for(var col=1; col <= this.lastCol; col++){
				var cell = this.getParentCell(col, row);
				if (cell != null && !ret.test(cell)){
					ret[arrayPos++] = cell;
				}
			}
		}
		return ret;
	}
	
	//returns an array with the position of the blank areas in the grid.
	//each element in the array is an object that contains the col and row of the blank area.
	this.getBlankAreas = function(){
		var blankAreas = new Array();
		for (var j=1; j <= this.colsCount; j++) {
			var lastRowInThisCol = this.getLastFreeRowInCol(j);
			for (var i=1; i < lastRowInThisCol; i++) {
				var cell = this.getParentCell(j, i);
				if (cell == null && this.getFreeRowAbovePos(j, i) == i) {
					blankAreas.push({col: j, row: i});
				}
			}
		}

		return blankAreas;
	}


	//debug only purpose
	this.notifyBlankAreas = function() {
		var blankAreas = this.getBlankAreas();
		blankAreas.each(function(blankArea){alert("Blank Area at ["+blankArea.col+"]["+blankArea.row+"]");},this);
	}
	//debug only purpose
	this.notifyPosition = function() {
		for (var i = 1; i <= this.lastRow; i++) {
			for (var j = 1; j <= this.lastCol; j++) {
				var cell = this.getParentCell(j, i);
				if (cell != null) {
					alert("Cell "+cell.div_id+" is in [row][col] ["+cell.row+"]["+cell.col+"]");
				}
			}
		}
	}
	
	
	this.blankCount = 0;
	//creates a blank cell and injects the DOM element into the DOM model.
	//Blank cells are used to be able to drop cells in blank areas.
	this.createBlankCell = function(){
		var div_element = new Element('div');
		div_element.id = 'blank-' + ++this.blankCount;
		div_element.injectAfter(this.cells_container_id);
		
		blankCellStyleNames.each(function(style){div_element.addClass(style);},this);
		div_element.setStyles({	'top': '1px', 'left': '1px', 'position': 'absolute', 'display': 'inline'});
		div_element.innerHTML = '<table cellspacing="0" cellpadding="0"><tr><td></td></tr></table>';
		
		var cell = new Cell(div_element.id, 'emptyCell', 1, false, true);

		cell.isBlankCell = true;

		return cell;
	}
	
	//creates a blank cell for each blank area in the grid, so the dragged cell can be dropped there
	this.completeBlanks = function(){
	
		//add blank cells at the bottom of each column
		for(var col=1; col <= this.colsCount; col++){
			var cell = this.createBlankCell();
			var row = this.getLastFreeRowInCol(col);
			this.addCell(col, row, cell);
		}

		//add blank cells between cells
		var blankAreas = this.getBlankAreas();
		blankAreas.each(function(blankArea){
				var cell = this.createBlankCell();
				this.addCell(blankArea.col, blankArea.row, cell);
			}, this);
	}
	
	//removes all the blank cells in the grid and removes its DOM elements form the DOM model.
	this.removeBlankCells = function(){
		var blankCells = this.getBlankCells();
		blankCells.each(function(cell){
				this.removeCell(cell);
				$(cell.div_id).removeEvents();
				$(cell.div_id).remove();
				this.blankCount--;
			}, this);
	}
	
	//returns an array with all the blank cells in the grid
	this.getBlankCells = function(){
		var allCells = this.getAllCells();
		var blankCells = new Array();
		allCells.each(function(cell){
				if (cell.isBlankCell) { blankCells.push(cell); };
			}, this);
		return blankCells;
	}

	//makes cells in the grid droppables for the given cell.
	//returns an array with the droppable cells. 	
	this.createDroppablesForCell = function(cell){
		var droppables = new Array();

		var allCells = this.getAllCells();
		allCells.each(function(aCell){
				if (cell != aCell){
					$(aCell.div_id).addEvents(droppableOptions);
					droppables.push($(aCell.div_id));
					if (aCell.isBlankCell == true){
						aCell.setHeight(cell.getHeight());
					}
				}
			},this);
		
		return droppables;
	}
	
	this.writeGrid = function() {
		var XML = new XMLWriter();
		XML.BeginNode("grid");
		XML.BeginNode("name");
			XML.WriteString(this.name);
		XML.EndNode();
		for(var i = 1; i <= this.lastRow; i++) {
			for (var j = 1; j <= this.lastCol; j++) {
				var cell = this.getCell(j, i);
				if (cell != null) {
					XML.BeginNode("cell");
					XML.BeginNode("contentPath");
					XML.WriteString(cell.contentPath);
					XML.EndNode();
					XML.BeginNode("col");
					XML.WriteString(cell.col);
					XML.EndNode();
					XML.BeginNode("row");
					XML.WriteString(cell.row);
					XML.EndNode();
					XML.BeginNode("colsUsed");
					XML.WriteString(cell.cols);
					XML.EndNode();
					XML.EndNode();
				}
			}
		}
		XML.EndNode();
		var xmlToString = XML.ToString();
//		alert(xmlToString);
		XML.Close();
		
		return xmlToString;
	}
	
	this.getLastFreeRowInCol = function(col){
		var cell;
		var row = this.lastRow;
		for( ; row > 0; row--){
			cell = this.getParentCell(col, row);
			if (cell != null){
				return row + 1;
			}
		}
		return 1;
	}
	
	//Add event listener for DOM ready event.
	var gridObject = this;
	window.onDomReady(function(){domIsReady(gridObject)});
}


/*
 * Represents a cell object.
 * Provides functionality to interact with the HTML element represented by this object.
 * Implementation note: the first child element of the HTML element must be a table in order to
 * obtain element dimensions correctly.
 */
function Cell(div_id, contentPath, cols, isEmpty, isActive){

	//No current use for this.
	this.cellName = '';
	//Id of the HTML element (not necesarily a div).
	this.div_id = div_id;
	//Used to know if the cell should be drawn or not.
	this.isEmpty = isEmpty;
	//No current use for this.
	this.isActive = isActive;
	//Cell column count. Used by the grid to position the cells.
	this.cols = cols;
	
	//Path to the component content
	this.contentPath = contentPath;
	
	//The current col and row where the cell is positioned.
	this.col = 0;
	this.row = 0;
	
	//Current absolute values for x and y position of the HTML element this cell represents.
	this.x = 0;
	this.y = 0;
	
	//Set the cell object for the HTML element represented by the cell so it can be used in drag&drop callbacks.
	$(this.div_id).cellObject = this;
	
	
	//Returns the HTML element real visible height.
	this.getHeight = function(){
		var cell_div = document.getElementById(this.div_id);
		return cell_div.getElementsByTagName('table')[0].clientHeight;
	}
	
	this.setHeight = function(height){
		var cell_div = document.getElementById(this.div_id);
		cell_div.getElementsByTagName('table')[0].style.height = height + 'px';
	}
	
	//Returns true if the cell should be drawn, false otherwise.
	this.show = function() {if (this.isEmpty)return false; return true; };
	
	//Positions the HTML element represented by this cell in the given x and y.
	this.setPosition = function(x, y){
		var cell_div = document.getElementById(this.div_id);
		cell_div.style.left = x + 'px';
		cell_div.style.top = y + 'px';
		this.x = x;
		this.y = y;
	}
	
	//Makes the cell draggable. Optionally, the id of the handle an be specified.
	this.makeCellDraggable = function(handleId){
		draggableOptions.handle = null;
		if (handleId && $(handleId)){
			draggableOptions.handle = $(handleId);
		}
		$(this.div_id).makeDraggable(draggableOptions);
	}
	
//	this.makeCellDraggable(this.div_id + '-drag');
}


//Returns the given element top position. This is cross-browser.
function getElementTop(id){
	var element = document.getElementById(id);
	
	var top = 0;
	if (element.offsetParent) {
		top = element.offsetTop;
		while (element = element.offsetParent) {
			top += element.offsetTop;
		}
	}
	return top;	
}

//Returns the given element left position. This is cross-browser.
function getElementLeft(id){
	var element = document.getElementById(id);
	
	var left = 0;
	if (element.offsetParent) {
		left = element.offsetLeft;
		while (element = element.offsetParent) {
			left += element.offsetLeft;
		}
	}
	return left;	
}

//This function is excecuted when the DOM tree is fully loaded.
//Note: Though the DOM tree is loaded, elements such as images might not be loaded yet.
function domIsReady(gridObject){
	gridObject.pushUp();
	gridObject.displayCells();
	gridObject.positionCells();

	//Add event on images load to position the cells.
	var images = document.getElementsByTagName('img');
	$$(images).each(function(el){
		el.addEvent('load', function(){gridObject.positionCells();});
	},this);

	//Add event on window resize to position the cells.
	window.addEvent('resize', function(){gridObject.positionCells();});
}
