/***
 * 
 * (c) 2009 Michael Leibman (michael.leibman@gmail.com)
 * All rights reserved.
 * 
 * 
 * TODO:
 * 	- frozen columns
 * 	- built-in row reorder
 * 	- add custom editor options
 * 	- consistent events (EventHelper?  jQuery events?)
 * 	- break resizeCanvas() into two functions to handle container resize and data size changes
 * 	- improve rendering speed by merging column extra cssClass into dynamically-generated .c{x} rules
 * 	- improve rendering speed by reusing removed row nodes and doing one .replaceChild() instead of two .removeChild() and .appendChild()
 * 
 * KNOWN ISSUES:
 * 	- keyboard navigation doesn't "jump" over unselectable cells for now
 *  - main page must have at least one STYLE element for jQuery Rule to work
 * 
 * 
 * OPTIONS:
 * 	enableAddRow			-	If true, a blank row will be displayed at the bottom - typing values in that row will add a new one.
 * 	manualScrolling			-	Disable automatic rerender on scroll.  Client will take care of calling Grid.onScroll().
 * 	editable				-	If false, no cells will be switched into edit mode.
 * 	editOnDoubleClick		-	Cell will not automatically go into edit mode without being double-clicked.
 * 	enableCellNavigation	-	If false, no cells will be selectable.
 * 	defaultColumnWidth		-	Default column width in pixels (if columns[cell].width is not specified).
 * 	enableColumnReorder		-	Allows the user to reorder columns.
 * 	asyncEditorLoading		-	Makes cell editors load asynchronously after a small delay.
 * 								This greatly increases keyboard navigation speed.
 * 	
 * 
 * COLUMN DEFINITION (columns) OPTIONS:
 * 	id						-	Column ID.
 * 	name					-	Column name to put in the header.
 * 	field					-	Property of the data context to bind to.
 * 	formatter				-	Function responsible for rendering the contents of a cell.
 * 	editor					-	An Editor class.
 * 	validator				-	An extra validation function to be passed to the editor.
 * 	unselectable			-	If true, the cell cannot be selected (and therefore edited).
 * 	cannotTriggerInsert		-	If true, a new row cannot be created from just the value of this cell.
 * 	setValueHandler			-	If true, this handler will be called to set field value instead of context[field].
 * 	width					-	Width of the column in pixels.
 * 	resizable				-	If false, the column cannot be resized.
 * 	minWidth				-	Minimum allowed column width for resizing.
 * 	maxWidth				-	Maximum allowed column width for resizing.
 * 	cssClass				-	A CSS class to add to the cell.
 * 	rerenderOnResize		-	Rerender the column when it is resized (useful for columns relying on cell width or adaptive formatters).
 * 	
 * 
 * EVENTS:
 * 
 * ...
 * 
 * 
 * NOTES:
 * 
 * 	Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods.
 * 	This increases the speed dramatically, but can only be done safely because there are no event handlers
 * 	or data associated with any cell/row DOM nodes.  Cell editors must make sure they implement .destroy() 
 * 	and do proper cleanup.
 * 
 * 
 * @param {jQuery} $container	Container object to create the grid in.
 * @param {Array} data			An array of objects for databinding.
 * @param {Array} columns		An array of column definitions.
 * @param {Object} options		Grid options.
 * 
 */

;( function( $ ) {

	// Private

	function SlickGrid($container, data, columns, options)
	{
		// consts
		var CAPACITY = 50;
		var BUFFER = 5;  // will be set to equal one page
	
		// private
		var uid = "slickgrid_" + Math.round(1000000 * Math.random());
		var self = this;
		var $gridContainer;
		var $divHeadersScroller;
		var $divMainScroller;
		var $divMain;
		var viewportH, viewportW;
		
		var currentRow, currentCell;
		var currentCellNode = null;
		var currentEditor = null;	
	
		var rowsCache = {};
		var renderedRows = 0;
		var numVisibleRows;
		var lastRenderedScrollTop = 0;
		var currentScrollTop = 0;
		var currentScrollLeft = 0;
		var scrollDir = 1;
		var avgRowRenderTime = 10;
	
		var selectedRows = [];
		var selectedRowsLookup = {};
		var columnsById = {};
	
		// async call handles
		var h_editorLoader = null;
		var h_render = null;
	
		// perf counters
		var counter_rows_rendered = 0;
		var counter_rows_removed = 0;
	
		var fragment = document.createDocumentFragment();
	
		// internal
		var _forceSyncScrolling = false;		
		var _selectionByClick = false;
	
	
		function init() {
			$gridContainer = $container;			
			$gridContainer
				.empty()
				.attr("tabIndex",0)
				.attr("hideFocus",true)
				.css("overflow","hidden")
				.css("outline",0)
				.addClass(uid);
		
			$divHeadersScroller = $("<div class='grid-header ui-widget-header' style='width:100%;height:1px;overflow:hidden;position:relative;' />").appendTo($gridContainer);
			$divMainScroller = $("<div class='grid-container' tabIndex='0' hideFocus style='width:100%;overflow-y:auto;overflow-x:hidden;outline:0px;position:relative;'>").appendTo($gridContainer);
			$divMain = $("<div class='grid-canvas' tabIndex='0' hideFocus />").appendTo($divMainScroller);
		
			$divMainScroller.height( $gridContainer.innerHeight() - $divHeadersScroller.outerHeight() );
			var gridWidth = $divMain.innerWidth();
			var columnWidths = 0;
			for (var i = 0; i < columns.length; i++) 
			{
				var m = columns[i];
			
				columnsById[m.id] = i;
			
				if (!m.width) {					
					m.width = options.defaultColumnWidth;
				}
				
				if (!m.formatter)
					m.formatter = defaultFormatter;
			
			}				
		
			resizeCanvas();
			if (options.selectedRow > -1) {
				gotoCell(options.selectedRow, 0);
			}
			render();
		
			if (!options.manualScrolling)
				$divMainScroller.bind("scroll", handleScroll);
		
			$divMain.bind("keydown", handleKeyDown);
			$divMain.bind("click", handleClick);
			$divMain.bind("dblclick", handleDblClick);

			if ($.browser.msie) 
				$divMainScroller[0].onselectstart = function() {
					if (event.srcElement.tagName != "INPUT" && event.srcElement.tagName != "TEXTAREA") 
						return false; 
					};
		}
	
		function destroy() {
			if (currentEditor)
				cancelCurrentEdit();
		
			$divHeaders.find(".h").resizable("destroy");
		
			$gridContainer.empty().removeClass(uid);
		}
	
		//////////////////////////////////////////////////////////////////////////////////////////////
		// General
	
		function setColumnHeaderCssClass(id,classesToAdd,classesToRemove) {
			$divHeaders.find(".h[id=" + id + "]").removeClass(classesToRemove).addClass(classesToAdd);
		}
	
		function getColumnIndex(id) {
			return columnsById[id];	
		}

		function getSelectedRows() {
			return selectedRows.concat();
		}	

		function setSelectedRows(rows) {
			if (GlobalEditorLock.isEditing() && !GlobalEditorLock.hasLock(self))
				throw "Grid : setSelectedRows : cannot set selected rows when somebody else has an edit lock";
		
			var lookup = {};
			for (var i=0; i<rows.length; i++)
				lookup[rows[i]] = true;
		
			// unselect old rows
			for (var i=0; i<selectedRows.length; i++)
			{
				var row = selectedRows[i];
				if (rowsCache[row] && !lookup[row]) {
					$(rowsCache[row]).removeClass("ui-state-active");					
					$(rowsCache[row]).removeClass("selected");
				}
			}

			// select new ones
			for (var i=0; i<rows.length; i++)
			{
				var row = rows[i];
				if (rowsCache[row] && !selectedRowsLookup[row]) {
					$(rowsCache[row]).addClass("ui-state-active");					
					$(rowsCache[row]).addClass("selected");
				}
			}

			selectedRows = rows.concat();
			selectedRowsLookup = lookup;				
		}

		function setOptions(args) {
			if (currentEditor && !commitCurrentEdit())
				return;
		
			setSelectedCell(null);
		
			if (options.enableAddRow != args.enableAddRow)
				removeRow(data.length);
			
			options = $.extend(options,args);		
		
			render();
		}
	
	
		//////////////////////////////////////////////////////////////////////////////////////////////
		// Rendering / Scrolling

		function defaultFormatter(row, cell, value, columnDef, dataContext) { 
			return (value == null || value == undefined) ? "" : value;
		}

		function appendRowHtml(stringArray,row) {
			var d = data[row];
			
			if (!d) {
				var t = data[row];
				alert('no d');
			}
			
			var dataLoading = row < data.length && !d;
			var css = "r ui-default-state" + (dataLoading ? " loading" : "") + (selectedRowsLookup[row] ? " selected ui-state-active" : "");
		
			stringArray.push("<div class='" + css + "' row='" + row + "' style='top:" + (options.row_height*row) + "px'>");
		
			for (var i=0, cols=columns.length; i<cols; i++) 
			{
				var m = columns[i];

				stringArray.push("<div " + (m.unselectable ? "" : "hideFocus tabIndex=0 ") + "class='c c" + i + (m.cssClass ? " " + m.cssClass : "") + "' style='width:" + columns[i].width + "px' cell=" + i + ">");

				// if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)				
				if (row < data.length && d)
					stringArray.push(m.formatter(row, i, d[m.field], m, d));
			
				stringArray.push("</div>");
			}
		
			stringArray.push("</div>");			
		}
	
		function getRowHtml(row) {
			var html = [];
		
			appendRowHtml(html,row);
		
			return html.join("");
		}
	
		function cleanupRows(visibleFrom,visibleTo) {
			console.time("cleanupRows");

			var rowsBefore = renderedRows;
			var parentNode = $divMain[0];
		
			for (var i in rowsCache)
			{
				if (i != currentRow && (i < visibleFrom || i > visibleTo))
				{
					parentNode.removeChild(rowsCache[i]);
				
					delete rowsCache[i];
					renderedRows--;		
				
					counter_rows_removed++;	
				}
			}
		
			console.log("removed " + (rowsBefore - renderedRows) + " rows");
			console.timeEnd("cleanupRows");
		}
	
		function removeAllRows() {
			console.time("removeAllRows");
		
			$divMain[0].innerHTML = "";
			rowsCache= {};
			counter_rows_removed += renderedRows;
			renderedRows = 0;
			console.timeEnd("removeAllRows");
		}	

		function removeRow(row) {
			var node = rowsCache[row];
			if (!node) return;
		
			if (currentEditor && currentRow == row)
				throw "Grid : removeRow : Cannot remove a row that is currently in edit mode";	
		
			// if we're removing rows, we're probably not scrolling
			scrollDir = 0;
		
			node.parentNode.removeChild(node);
			node = null;
				
			delete rowsCache[row];	
			renderedRows--;
		
			counter_rows_removed++;
		}
	
		function removeRows(rows) {
			console.time("removeRows");

			if (!rows || !rows.length) return;
		
			scrollDir = 0;
			var nodes = [];
		
			for (var i=0, rl=rows.length; i<rl; i++) {
				if (currentEditor && currentRow == i)
					throw "Grid : removeRow : Cannot remove a row that is currently in edit mode";	
			
				var node = rowsCache[rows[i]];
				if (!node) continue;
			
				nodes.push(rows[i]);
			}
		
			if (nodes.length == renderedRows) {
				$divMain[0].innerHTML = "";
				rowsCache= {};
				counter_rows_removed += renderedRows;
				renderedRows = 0;			
			} else {
				for (var i=0, nl=nodes.length; i<nl; i++) {
					var node = rowsCache[nodes[i]];
					node.parentNode.removeChild(node);
					delete rowsCache[nodes[i]];
					renderedRows--;
					counter_rows_removed++;
				}
			}
		
			console.timeEnd("removeRows");
		}
	
		function updateCell(row,cell) {
			if (!rowsCache[row]) return;
			var $cell = $(rowsCache[row]).find(".c[cell=" + cell + "]");
			if ($cell.length == 0) return;
		
			var m = columns[cell];	
			var d = data[row];	
		
			if (currentEditor && currentRow == row && currentCell == cell)
				currentEditor.setValue(d[m.field]);
			else 
				$cell[0].innerHTML = d ? m.formatter(row, cell, d[m.field], m, d) : ""
		}

		function updateRow(row) {
			if (!rowsCache[row]) return;
		
			// todo:  perf:  iterate over direct children?
			$(rowsCache[row]).find(".c").each(function(i) {
				var m = columns[i];
			
				if (row == currentRow && i == currentCell && currentEditor)
					currentEditor.setValue(data[currentRow][m.field]);
				else if (data[row])
					this.innerHTML = m.formatter(row, i, data[row][m.field], m, data[row]);
				else
					this.innerHTML = "";
			});
		}

		function resizeCanvas() {
			
			$divMainScroller.height( $gridContainer.innerHeight() - $divHeadersScroller.outerHeight() );
			
			viewportW = $divMainScroller.innerWidth();
			viewportH = $divMainScroller.innerHeight();

			BUFFER = numVisibleRows = Math.ceil(viewportH / options.row_height);
			CAPACITY = Math.max(CAPACITY, numVisibleRows + 2*BUFFER);

			$divMain.height(options.row_height * data.length);

			var gridWidth = $divMain.innerWidth() - 5;
			var columnWidths = 0;
			for (var i = 0; i < columns.length; i++) 
			{
				var m = columns[i];
 				if ((columnWidths + m.width + 5) > gridWidth) {
					m.width = (gridWidth - columnWidths) - 5;
				}					
				columnWidths += m.width + 5;
			}				
			
			// remove the rows that are now outside of the data range
			// this helps avoid redundant calls to .removeRow() when the size of the data decreased by thousands of rows
			var parentNode = $divMain[0];
			var l = options.enableAddRow ? data.length : data.length - 1;
			for (var i in rowsCache)
			{
				if (i >= l)
				{
					parentNode.removeChild(rowsCache[i]);
					delete rowsCache[i];
					renderedRows--;
				
					counter_rows_removed++;
				}
			}

			// browsers sometimes do not adjust scrollTop/scrollHeight when the height of contained objects changes
			if ($divMainScroller.scrollTop() > $divMain.height() - $divMainScroller.height())
				$divMainScroller.scrollTop($divMain.height() - $divMainScroller.height());
		}
	
		function getViewport()
		{
			return {
				top:	Math.floor(currentScrollTop / options.row_height),
				bottom:	Math.floor((currentScrollTop + viewportH) / options.row_height)
			};	
		}
	
		function renderRows(from,to) {
			console.time("renderRows");

			if (options.onRenderRows)
				options.onRenderRows(data, from, to);
		
			var rowsBefore = renderedRows;
			var stringArray = [];
			var _start = new Date();
			var x = document.createElement("div");
		
			var rows =[];
		
			for (var i = from; i <= to; i++) {
				if (rowsCache[i]) continue;
				renderedRows++;
			
				rows.push(i);
				appendRowHtml(stringArray,i);

				counter_rows_rendered++;
			}
		
			x.innerHTML = stringArray.join("");
		
			for (var i = 0, nodes = x.childNodes, l = nodes.length; i < l; i++) {
				rowsCache[rows[i]] = $divMain[0].appendChild(x.firstChild);
			}
		
			if (renderedRows - rowsBefore > 5)
				avgRowRenderTime = (new Date() - _start) / (renderedRows - rowsBefore);
		
			console.log("rendered " + (renderedRows - rowsBefore) + " rows");
			console.timeEnd("renderRows");		
		}	
	
		function setData(rows) {
			data = rows;
		}
		
		function render() {
			var vp = getViewport();
			var from = Math.max(0, vp.top - (scrollDir >= 0 ? 5 : BUFFER));
			var to = Math.min(options.enableAddRow ? data.length : data.length - 1, vp.bottom + (scrollDir > 0 ? BUFFER : 5));

			if (Math.abs(lastRenderedScrollTop - currentScrollTop) > options.row_height*CAPACITY)
				removeAllRows();
			else if (renderedRows >= CAPACITY)
				cleanupRows(from,to);

			renderRows(from,to);

			lastRenderedScrollTop = currentScrollTop;
			h_render = null;
		}

		function handleScroll() {
			currentScrollTop = $divMainScroller[0].scrollTop;
			var scrollDistance = Math.abs(lastRenderedScrollTop - currentScrollTop);
			var scrollLeft = $divMainScroller[0].scrollLeft;
		
			if (scrollLeft != currentScrollLeft)
				$divHeadersScroller[0].scrollLeft = currentScrollLeft = scrollLeft;
		
			window.status = currentScrollLeft;
		
			if (scrollDistance < 5*options.row_height) return;
		
			if (lastRenderedScrollTop == currentScrollTop)
				scrollDir = 0;
			else if (lastRenderedScrollTop < currentScrollTop)
				scrollDir = 1;
			else	
				scrollDir = -1;
		
			if (h_render)
				window.clearTimeout(h_render);
		
			if (scrollDistance < 2*numVisibleRows*options.row_height || avgRowRenderTime*CAPACITY < 30 ||  _forceSyncScrolling) 
				render();
			else
				h_render = window.setTimeout(render, 50);
			
			if (options.onViewportChanged)
				options.onViewportChanged();
		}


		//////////////////////////////////////////////////////////////////////////////////////////////
		// Interactivity

		function handleKeyDown(e) {
//			alert('handleKeyDown');
			switch (e.which) {
				case 27:  // esc
					if (GlobalEditorLock.isEditing() && GlobalEditorLock.hasLock(self))
						cancelCurrentEdit(self);
				
					if (currentCellNode)
						currentCellNode.focus();
				
					break;
			
				case 9:  // tab
					if (e.shiftKey)
						gotoDir(0,-1,true);	//gotoPrev();
					else
						gotoDir(0,1,true);	//gotoNext();
					
					break;
				
				case 37:  // left
					gotoDir(0,-1);
					break;
				
				case 39:  // right
					gotoDir(0,1);
					break;
				
				case 38:  // up
					gotoDir(-1,0);
					break;
				
				case 40:  // down
				case 13:  // enter
					gotoDir(1,0);
					break;
								
				default:

					// do we have any registered handlers?
					if (options.onKeyDown && data[currentRow])
					{
						// grid must not be in edit mode
						if (!currentEditor) 
						{
							// handler will return true if the event was handled
							if (options.onKeyDown(e, currentRow, currentCell)) 
							{
								e.stopPropagation();
								e.preventDefault();
								return false;
							}
						}
					}			
			
					// exit without cancelling the event
					return;
			}
		
			e.stopPropagation();
			e.preventDefault();
			return false;		
		}	
	
		function handleClick(e)
		{
//			alert('handleClick');
			var $cell = $(e.target).closest(".c");
		
			if ($cell.length == 0) return;
		
			// are we editing this cell?
			if (currentCellNode == $cell[0] && currentEditor != null) return;
		
			var row = parseInt($cell.parent().attr("row"));
			var cell = parseInt($cell.attr("cell"));		
	
			var validated = null;
	
			// do we have any registered handlers?
			if (data[row] && options.onClick)
			{
				// grid must not be in edit mode
				if (!currentEditor || (validated = commitCurrentEdit())) 
				{
					// handler will return true if the event was handled
					if (options.onClick(e, row, cell)) 
					{
						e.stopPropagation();
						e.preventDefault();
						return false;
					}
				}
			}


			if (options.enableCellNavigation && !columns[cell].unselectable) 
			{
				// commit current edit before proceeding
				if (validated == true || (validated == null && commitCurrentEdit())) {
					_selectionByClick = true;
					setSelectedCellAndRow($cell[0]);
					_selectionByClick = false;
				}
			}
			
			if (options.enableRowNavigation)
			{
				// commit current edit before proceeding
				if (validated == true || (validated == null && commitCurrentEdit())) {
					_selectionByClick = true;
					setSelectedCellAndRow($cell[0]);
					_selectionByClick = false;
				}
			}
		}
	
		function handleDblClick(e)
		{
			var $cell = $(e.target).closest(".c");
		
			if ($cell.length == 0) return;
		
			// are we editing this cell?
			if (currentCellNode == $cell[0] && currentEditor != null) return;
				
			if (options.editOnDoubleClick)
				editCurrentCell();
		}

		function getCellFromPoint(x,y) {
			var row = Math.floor(y/options.row_height);
			var cell = 0;
		
			var w = 0;		
			for (var i=0; i<columns.length && w<y; i++)
			{
				w += columns[i].width;
				cell++;
			}
		
			return {row:row,cell:cell-1};
		}


		//////////////////////////////////////////////////////////////////////////////////////////////
		// Cell switching
	
		function setSelectedCell(newCell,async)
		{
			if (currentCellNode != null) 
			{
				makeSelectedCellNormal();			
			
				$(currentCellNode).removeClass("selected");
				$(currentCellNode).removeClass("ui-state-active");
			}
		
			currentCellNode = newCell;
		
			if (currentCellNode != null) 
			{
				currentRow = parseInt($(currentCellNode).parent().attr("row"));
				currentCell = parseInt($(currentCellNode).attr("cell"));
			
				//$(currentCellNode).addClass("selected");
				scrollSelectedCellIntoView();
			
				if (options.editable && !options.editOnDoubleClick && (data[currentRow] || currentRow == data.length)) 
				{
					window.clearTimeout(h_editorLoader);
				
					if (async) 
						h_editorLoader = window.setTimeout(editCurrentCell, 100);
					else 
						editCurrentCell();
				}
			}
			else
			{
				currentRow = null;
				currentCell = null;	
			}
		}
	
		function setSelectedCellAndRow(newCell,async) {
			setSelectedCell(newCell,async);
		
			if (newCell) 
				setSelectedRows([currentRow]);
			else
				setSelectedRows([]);
			
			if (options.onSelectedRowsChanged)
				options.onSelectedRowsChanged();			
		}
	
		function clearTextSelection()
		{
			if (document.selection && document.selection.empty) 
				document.selection.empty();
			else if (window.getSelection) 
			{
				var sel = window.getSelection();
				if (sel && sel.removeAllRanges) 
					sel.removeAllRanges();
			}
		}	

		function isCellPotentiallyEditable(row,cell) {
			// is the data for this row loaded?
			if (row < data.length && !data[row])
				return false;
		
			// are we in the Add New row?  can we create new from this cell?
			if (columns[cell].cannotTriggerInsert && row >= data.length)
				return false;
			
			// does this cell have an editor?
			if (!columns[cell].editor)
				return false;
			
			return true;		
		}

		function makeSelectedCellNormal() {
			if (!currentEditor) return;
					
			currentEditor.destroy();
			$(currentCellNode).removeClass("editable invalid");
		
			if (data[currentRow]) 
				currentCellNode.innerHTML = columns[currentCell].formatter(currentRow, currentCell, data[currentRow][columns[currentCell].field], columns[currentCell], data[currentRow]);
		
			currentEditor = null;
		
			// if there previously was text selected on a page (such as selected text in the edit cell just removed),
			// IE can't set focus to anything else correctly
			if ($.browser.msie) clearTextSelection();

			GlobalEditorLock.leaveEditMode(self);		
		}

		function editCurrentCell()
		{
			if (!currentCellNode) return;
		
			if (!options.editable)
				throw "Grid : editCurrentCell : should never get called when options.editable is false";
		
			// cancel pending async call if there is one
			window.clearTimeout(h_editorLoader);
		
			if (!isCellPotentiallyEditable(currentRow,currentCell))
				return;

			GlobalEditorLock.enterEditMode(self);

			$(currentCellNode).addClass("editable");
		
			var value = null;
	
			// if there is a corresponding row
			if (data[currentRow])
				value = data[currentRow][columns[currentCell].field];

			currentCellNode.innerHTML = "";
		
			currentEditor = new columns[currentCell].editor($(currentCellNode), columns[currentCell], value, data[currentRow]);
		}

		function scrollSelectedCellIntoView() {
			if (!currentCellNode) return;
			if (_selectionByClick) return;
		

			var scrollTop = $divMainScroller[0].scrollTop;
		
			// need to page down?
			if ((currentRow * options.row_height > scrollTop + (viewportH * 2 / 3)) && scrollDir != -1) 
			{
				if (scrollDir == 1) {
					$divMainScroller[0].scrollTop = currentRow * options.row_height - (viewportH * 2 / 3);
				} else {
					// center
					$divMainScroller[0].scrollTop = currentRow * options.row_height - (viewportH / 2);
				}
			
				handleScroll();
			}
			// or page up?
			else if ((currentRow * options.row_height < scrollTop + (viewportH / 3)) && scrollDir != 1)
			{
				if (scrollDir == -1) {
					$divMainScroller[0].scrollTop = currentRow * options.row_height - (viewportH / 3);
				} else  {
					//center
					$divMainScroller[0].scrollTop = currentRow * options.row_height - (viewportH / 2);
				}
			
				handleScroll();			
			}
			else {
//				alert('in which direction: scrollDir:' + scrollDir 
//						+ ' needed: ' + (currentRow * options.row_height) 
//						+ ' given from: ' + scrollTop
//						+ ' to ' + (scrollTop + viewportH));
			}
		}

		function gotoDir(dy, dx, rollover)
		{
			if (!currentCellNode) return;
			if (!options.enableCellNavigation && !options.enableRowNavigation) return;		
			if (!GlobalEditorLock.commitCurrentEdit()) return;
		
			if (dy > 0) {
				scrollDir = 1;
			} else if (dy < 0) {
				scrollDir = -1;
			} else {
				scrollDir = 0;
			}
			
			var nextRow = rowsCache[currentRow + dy];
			var nextCell = nextRow ? $(nextRow).find(".c[cell=" + (currentCell + dx) + "][tabIndex=0]") : null;
		
			if (rollover && dy == 0 && !(nextRow && nextCell && nextCell.length))
			{
				if (!nextCell || !nextCell.length)
				{
					if (dx > 0) 
					{
						nextRow = rowsCache[currentRow + dy + 1];
						nextCell = nextRow ? $(nextRow).find(".c[cell][tabIndex=0]:first") : null;						
					}
					else
					{
						nextRow = rowsCache[currentRow + dy - 1];
						nextCell = nextRow ? $(nextRow).find(".c[cell][tabIndex=0]:last") : null;		
					}
				}
			}
		
		
			if (nextRow && nextCell && nextCell.length) 
			{
				setSelectedCellAndRow(nextCell[0],options.asyncEditorLoading);
			
				// if no editor was created, set the focus back on the cell
				if (!currentEditor) 
					currentCellNode.focus();
			}
			else 
				currentCellNode.focus();
		}

		function gotoCell(row,cell)
		{
			if (row > data.length || row < 0 || cell >= columns.length || cell < 0) return;
			if (!options.enableCellNavigation || columns[cell].unselectable) return;
		
			if (!GlobalEditorLock.commitCurrentEdit()) return;
		
			if (!rowsCache[row]) {
				renderRows(row,row);
			}
		
			var cell = $(rowsCache[row]).find(".c[cell=" + cell + "][tabIndex=0]")[0];
			
			scrollDir = null;
			setSelectedCellAndRow(cell);		
		}


		//////////////////////////////////////////////////////////////////////////////////////////////
		// IEditor implementation for GlobalEditorLock	
	
		function commitCurrentEdit() {
			if (currentEditor)
			{
				if (currentEditor.isValueChanged())
				{
					var validationResults = currentEditor.validate();
				
					if (validationResults.valid) 
					{
						var value = currentEditor.getValue();
					
						if (currentRow < data.length) {
							if (columns[currentCell].setValueHandler) {
								makeSelectedCellNormal();
								columns[currentCell].setValueHandler(value, columns[currentCell], data[currentRow]);
							}
							else {
								data[currentRow][columns[currentCell].field] = value;
								makeSelectedCellNormal();
							}
						}
						else if (options.onAddNewRow) {
								makeSelectedCellNormal();
								options.onAddNewRow(columns[currentCell], value);
							}
					
						return true;
					}
					else 
					{
						$(currentCellNode).addClass("invalid");
						$(currentCellNode).stop(true,true).effect("highlight", {color:"red"}, 300);
					
						if (options.onValidationError)
							options.onValidationError(currentCellNode, validationResults, currentRow, currentCell, columns[currentCell]);
					
						currentEditor.focus();
						return false;
					}
				}
			
				makeSelectedCellNormal();
			}
		
		
			return true;
		};
	
		function cancelCurrentEdit() {
			makeSelectedCellNormal();
		};
	
	
	
		//////////////////////////////////////////////////////////////////////////////////////////////
		// Debug
	
		this.debug = function() {
			var s = "";
		
			s += ("\n" + "counter_rows_rendered:  " + counter_rows_rendered);	
			s += ("\n" + "counter_rows_removed:  " + counter_rows_removed);	
			s += ("\n" + "renderedRows:  " + renderedRows);	
			s += ("\n" + "numVisibleRows:  " + numVisibleRows);			
			s += ("\n" + "CAPACITY:  " + CAPACITY);			
			s += ("\n" + "BUFFER:  " + BUFFER);		
			s += ("\n" + "avgRowRenderTime:  " + avgRowRenderTime);
		
			alert(s);
		};
	
		this.benchmark_render_200 = function() {
			removeAllRows();
		
			// render 200 rows in the viewport
			renderRows(0, 200);
		
			cleanupRows();
		};
	
		this.stressTest = function() {
			console.time("benchmark-stress");

			renderRows(0,500);
		
			cleanupRows();
		
			console.timeEnd("benchmark-stress");
		
			window.setTimeout(self.stressTest, 50);
		};
	
		this.benchmarkFn = function(fn) {
			var s = new Date();
		
			var args = new Array(arguments);
			args.splice(0,1);
		
			self[fn].call(this,args);
		
			alert("Grid : benchmarkFn : " + fn + " : " + (new Date() - s) + "ms");		
		};




		init();	
	
	
		//////////////////////////////////////////////////////////////////////////////////////////////
		// Public API

		$.extend(this, {
			// SlickGrid Methods
			"destroy":			destroy,
			"getColumnIndex":	getColumnIndex,
			"updateCell":		updateCell,
			"updateRow":		updateRow,
			"removeRow":		removeRow,
			"removeRows":		removeRows,
			"removeAllRows":	removeAllRows,
			"render":			render,
			"setData":			setData,
			"getViewport":		getViewport,
			"resizeCanvas":		resizeCanvas,
			"scroll":			scroll,  // TODO
			"getCellFromPoint":	getCellFromPoint,
			"gotoCell":			gotoCell,
			"editCurrentCell":	editCurrentCell,
			"getSelectedRows":	getSelectedRows,
			"setSelectedRows":	setSelectedRows,
			"setColumnHeaderCssClass":	setColumnHeaderCssClass,
		
			// IEditor implementation
			"commitCurrentEdit":	commitCurrentEdit,
			"cancelCurrentEdit":	cancelCurrentEdit
		});
	};


	//////////////////////////////////////////////////////////////////////////////////////////////
	// jQuery UI Widget

	var SlickGridWidget = {
		_grid: null,

		_init: function() {
			_grid = new SlickGrid(this.element, this.options.rows, this.options.columns, this.options);
		},

		destroy: function() { 
			_grid.destroy();
			_grid = null;
		},

		// Methods
		getColumnIndex: function() { return _grid.getColumnIndex(); },
		updateCell: function(row, cell) { _grid.updateCell(row, cell); },
		updateRow: function(row) { _grid.updateRow(row); },
		removeRow: function(row) { _grid.removeRow(row); },
		removeRows: function(rows) { _grid.removeRows(rows); },
		removeAllRows: function(rows) { _grid.removeAllRows(); },
		render: function() { _grid.render(); },
		setData: function(rows) { _grid.setData(rows); },
		getViewport: function() { return _grid.getViewport(); },
		resizeCanvas: function() { _grid.resizeCanvas(); },
		//scroll: _grid.scroll,  // TODO
		getCellFromPoint: function(x, y) { return _grid.getCellFromPoint(x, y); },
		gotoCell: function(row, cell) { _grid.gotoCell(row, cell); },
		editCurrentCell: function() { _grid.editCurrentCell(); },
		getSelectedRows: function() { return _grid.getSelectedRows(); },
		setSelectedRows: function(rows) { _grid.setSelectedRows(rows); },
		setColumnHeaderCssClass: function(id,classesToAdd,classesToRemove) {
			_grid.setColumnHeaderCssClass(id,classesToAdd,classesToRemove);
		},
	
		// IEditor implementation
		commitCurrentEdit: function() { _grid.commitCurrentEdit(); },
		cancelCurrentEdit: function() { _grid.cancelCurrentEdit(); }
	};

	// Plugin definition
	$.widget("ui.grid", SlickGridWidget);

	$.extend($.ui.grid, {
		version: "0.9",
		eventPrefix: "grid",
		getter: ["getColumnIndex", "getViewport", "getCellFromPoint", "getSelectedRows"],
		defaults: {
			row_height: 24,
			enableAddRow: true,
			manualScrolling: false,
			editable: true,
			editOnDoubleClick: false,
			enableCellNavigation: true,
			defaultColumnWidth: 80,
			enableColumnReorder: true,
			asyncEditorLoading: true,
			
			// Event handlers:
			onColumnHeaderClick: null,
			onClick: null,
			onKeyDown: null,
			onAddNewRow: null,
			onValidationError: null,
			onViewportChanged: null,
			onSelectedRowsChanged: null,
			onColumnsReordered: null
		}
	});

})(jQuery);

