/* --------- BEGIN LICENSE NOTICE ---------
 * Copyright 2011 Extentech Inc. All Rights Reserved.
 *
 * This file is a part of the Sheetster Web Application.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * If you would like to redistribute this software in a closed-source
 * application, dual-licensed commercial versions are available. For a
 * fully supported and redistributable commercial license, please visit
 * <http://www.extentech.com> or contact us at:
 * 
 * sales@extentech.com
 * Extentech Inc.
 * 1032 Irving Street #910
 * San Francisco, CA 94122
 * 415-759-5292
 * ---------- END LICENSE NOTICE ----------
 */

/** @class Represents a contiguous rectangular group of cells.
 * Instances of this class are immutable in the same manner as strings. Once
 * created, the range to which they refer cannot be changed. Methods are
 * provided for deriving related ranges. 
 * 
 * @description Produces a <code>cellRange</code> instance.
 * There are several supported ways of specifying the range. 
 * 
 * <p>If one argument is given, it may either be an Excel-style range string or
 * a handle object. In the case of a handle object, if will be the sole element
 * in the range.</p>
 * 
 * <p>If two or more arguments are given, the first two will be interpreted as
 * the bounding elements of the range. They may be either handle objects or
 * Excel-style element address strings. If <code>null</code> is passed for the
 * second bound a single-element range will be created.<p>
 * 
 * <p>If three or more arguments are given, the third and fourth will be
 * interpreted as the bounding sheets of the range. They may be either
 * {@link sheetHandle} objects or sheet name strings.</p>
 */
cellRange = Class.create( Enumerable,
/** @lends cellRange.prototype */ {
	/** @ignore */
	initialize: function() {
		this.name = null; // used if it's a named range
		this.range = {};
		this.firstSheet = {};
		this.lastSheet = {};
		this.first = {};
		this.last = {};
		
		if (arguments.length === 0)
			throw new ContextualError(
					"can't construct an empty range", 'InvalidArgument' );
		
		if (arguments.length >= 4) 
			throw new ContextualError(
					'too many arguments', 'InvalidArgument' );
		
		if (typeof arguments[ 0 ] === 'string') {
			// if it's the only argument it's an Excel-style range
			if (arguments.length === 1) {
				this.range = cellRange.splitSheets( arguments[ 0 ] );
				
				var split = cellRange.splitRange( this.range.cells );
				this.first.addr = split.first;
				if (split.last) this.last.addr = split.last;
				
				if (this.range.sheets) {
					split = cellRange.splitRange( this.range.sheets );
					this.firstSheet.name = split.first;
					if (split.last) this.lastSheet.name = split.last;
				}
			}
			
			// otherwise it's an element address
			else this.first.addr = arguments[ 0 ];
		}
		
		else if (cellRange.isElementHandle( arguments[ 0 ] ))
			this.first.handle = arguments[ 0 ];
		else throw new ContextualError(
				'unsupported type passed for first bounding element',
				'TypeError', 'IllegalArgument' );
			
		if (arguments.length >= 2 && arguments[ 1 ] !== null) {
			if (typeof arguments[ 1 ] === 'string')
				this.last.addr = arguments[ 1 ];
			else if (cellRange.isElementHandle( arguments[ 1 ] ))
				this.last.handle = arguments[ 1 ];
			else throw new ContextualError(
					'unsupported type passed for last bounding element',
					'TypeError', 'IllegalArgument' );
		} else if (!this.last.addr) {
			this.last = this.first;
		}
		
		if (arguments.length >= 3 && arguments[ 2 ] !== null) {
			if (typeof arguments[ 2 ] === 'string')
				this.firstSheet.name = arguments[ 2 ];
			else if (arguments[ 2 ] instanceof sheetHandle)
				this.firstSheet.handle = arguments[ 2 ];
			else throw new ContextualError(
					'unsupported type passed for first bounding sheet',
					'TypeError', 'IllegalArgument' );
		} else if (!this.firstSheet.name) {
			this.firstSheet.handle = sheet;
			this.range = {};
		}
		
		if (arguments.length >= 4 && arguments[ 3 ] !== null) {
			if (typeof arguments[ 3 ] === 'string')
				this.lastSheet.name = arguments[ 3 ];
			else if (arguments[ 3 ] instanceof sheetHandle)
				this.lastSheet.handle = arguments[ 3 ];
			else throw new ContextualError(
					'unsupported type passed for last bounding sheet',
					'TypeError', 'IllegalArgument' );
		} else if (!this.lastSheet.name) {
			this.lastSheet = this.firstSheet;
		}
		
		// determine the range type
		this.type = cellRange.getElementType( this.first );
		
		if (this.last !== this.first) {
			// check for type mismatch
			if (cellRange.getElementType( this.last ) !== this.type)
				throw new ContextualError(
						'element type mismatch', 'TypeError' )
						.add( 'first type', this.first.type )
						.add( 'last type', this.last.type );
		
			// normalize the bounding elements
			if (this.type === 'cell') {
				var rc1 = cellRange.getElementRowCol( this.first );
				var rc2 = cellRange.getElementRowCol( this.last );
				
				// if we have NE and SW, switch to NW and SE
				if ((rc1.row <= rc2.row) != (rc1.col <= rc2.col)) {
					this.first = { type: 'cell' };
					this.first.addr = toolkit.formatLocation( [
               				Math.min( rc1.row, rc2.row ),
            				Math.min( rc1.col, rc2.col )
            			] );
					
					this.last = { type: 'cell' };
					this.last.addr = toolkit.formatLocation( [
                  			Math.max( rc1.row, rc2.row ),
               				Math.max( rc1.col, rc2.col )
               			] );
					
					this.range = {};
				} 
				
				// if the bounds are out of order, swap them
				else if (rc1.row > rc2.row || rc1.col > rc2.col) {
					var swap = this.first;
					this.first = this.last;
					this.last = swap;
					this.range = {};
				}
			} else {
				// if the bounds are out of order, swap them
				if (cellRange.getElementIndex( this.first )
						> cellRange.getElementIndex( this.last ) ) {
					var swap = this.first;
					this.first = this.last;
					this.last = swap;
					this.range = {};
				}	
			}
		}
	}

	/** Gets the range type.
	 * @return {string} one of <code>'cell'</code>, <code>'column'</code>,
	 *         or <code>'row'</code>
	 */
	, getType: function() {
		return this.type;
	}
	
	, isCellRange: function() {
		return this.type === 'cell';
	}
	
	, isColumnRange: function() {
		return this.type === 'column';
	}
	
	, isRowRange: function() {
		return this.type === 'row';
	}
	
	/** Returns whether the range contains exactly one cell.
	 * 
	 */
	, isSingle: function() {
		return this.last === this.first;
	}
	
	, isMultiple: function() {
		return this.last !== this.first;
	}
	
	, isMultiCell: function() {
		return this.type !== 'cell' || this.isMultiple();
	}
	
	/** Returns whether the entire range is within the upper section
	 *of a split sheet
	 */
	, isWithinTopSplit: function() {
		var swcell = this.getCellSW();
		if(swcell.getRowCol()[0]<=sheet.getSplitRow())return true;
		return false;
	}
	
	/** Gets the first bounding element.
	 * @return {cellHandle|rowHandle|colHandle} a handle to the first bound  
	 */
	, getFirst: function() {
		return this.getElementHandle( this.first );
	}
	
	/** Gets the last bounding element.
	 * @return {cellHandle|rowHandle|colHandle} a handle to the last bound  
	 */
	, getLast: function() {
		return this.getElementHandle( this.last );
	}
	
	/** Gets the first bounding sheet.
	 * @return {string} the name of the first sheet
	 */
	, getFirstSheet: function() {
		if (this.firstSheet.name) return this.firstSheet.name;
		else return this.firstSheet.handle.getSheetName();
	}
	
	/** Gets the last bounding sheet.
	 * @return {string} the name of the last sheet
	 */
	, getLastSheet: function() {
		if (this.lastSheet.name) return this.lastSheet.name;
		else return this.lastSheet.handle.getSheetName();
	}
	
	/** Checks within the given element is within this range.
	 * @param {cellHandle|rowHandle|colHandle} elem the element handle to check
	 * @return {boolean} whether the given element falls within this range
	 */
	, contains: function (elem) {
		if (this.type === 'row' || this.type === 'column') {
			var index;
			if (elem instanceof cellHandle)
				index = elem.getRowCol()[
				        this.type === 'column' ? 'col' : 'row' ];
			else index = elem.getIndex();
			
			return index >= cellRange.getElementIndex( this.first )
					&& index <= cellRange.getElementIndex( this.last );
		}
		
		if (!(elem instanceof cellHandle)) throw new ContextualError(
				'elem must be a cellHandle', 'TypeError', 'InvalidArgument' );
		
		var rangeRC = this.getRowCols();
		var cellRC = elem.getRowCol();
		
		return cellRC.col >= rangeRC.left && cellRC.col <= rangeRC.right
				&& cellRC.row >= rangeRC.top && cellRC.row <= rangeRC.bottom;
	}
	
	/** Compares this range to another one for equality.
	 * @param {cellRange} that the range to compare against
	 * @return whether both range objects represent the same range
	 */
	, equals: function (that) {
		return this.type === that.type
				&& this.getFirstSheet() === that.getFirstSheet()
				&& this.getLastSheet() === that.getLastSheet()
				&& cellRange.getElementAddress( this.first )
					=== cellRange.getElementAddress( that.first )
				&& cellRange.getElementAddress( this.last )
					=== cellRange.getElementAddress( that.last );
	}

	/** Gets the cell in the top left (north-west) corner of the range.
	 * @return {cellHandle} the farthest top left cell in the range
	 */
	, getCellNW: function() {
		if (this.type === 'row') {
			return this.getElementHandle({ type: 'cell',
				rowcol: [ cellRange.getElementIndex( this.first ), 0 ] });
		}
			
		if (this.type === 'column') {
			return this.getElementHandle({ type: 'cell',
				rowcol: [ 0, cellRange.getElementIndex( this.first ) ] });
		}
				
		return this.getElementHandle( this.first );
	}
	
	/** Gets the cell in the top right (north-east) corner of the range.
	 * @return {cellHandle} the farthest top right cell in the range
	 */
	, getCellNE: function() {
		if (this.type === 'row') throw new ContextualError(
				'row selections have no east edge', 'Unsupported' );
		
		if (this.type === 'column') {
			return this.getElementHandle({ type: 'cell',
				rowcol: [ 0, cellRange.getElementIndex( this.last ) ] });
		}
		
		return sheet.getCell( toolkit.formatLocation( [
   				cellRange.getElementRowCol( this.first ).row,
   				cellRange.getElementRowCol( this.last  ).col
   			] ) );
	}
	
	/** Gets the cell in the bottom left (south-west) corner of the range.
	 * @return {cellHandle} the farthest bottom left cell in the range
	 */
	, getCellSW: function() {
		if (this.type === 'row') {
			return this.getElementHandle({ type: 'cell',
				rowcol: [ cellRange.getElementIndex( this.last ), 0 ] });
		}
		
		if (this.type === 'column') throw new ContextualError(
				'column selections have no south edge', 'Unsupported' );
		
		return sheet.getCell( toolkit.formatLocation( [
				cellRange.getElementRowCol( this.last  ).row,
				cellRange.getElementRowCol( this.first ).col
			] ) );
	}
	
	/** Gets the cell in the bottom right (south-east) corner of the range.
	 * @return {cellHandle} the farthest bottom right cell in the range
	 */
	, getCellSE: function() {
		if (this.type === 'row') throw new ContextualError(
				'row selections have no east edge', 'Unsupported' );
		
		if (this.type === 'column') throw new ContextualError(
				'column selections have no south edge', 'Unsupported' );
		
		return this.getElementHandle( this.last );
	}
	
	/** Returns whether any cell in the range is locked.
	 * This method currently takes into account the sheet protection status.
	 * That is, if the sheet is not protected it will return false even if a
	 * cell is locked.
	 */
	, isLocked: function() {
		return this.any( function (cell) {
				if(typeof(cell.isLocked)!='undefined')
					return cell.isLocked();
				else
					return false;
			});
	}
	
	/** Gets the cell directly opposite the given cell in the range.
	 * @param {cellHandle} base the cell whose opposite should be found
	 * @return {cellHandle} the cell directly opposite the given cell
	 */
	, getOppositeCell: function (base) {
		if (!(base instanceof cellHandle)) throw new ContextualError(
				'base must be a cellHandle', 'TypeError', 'InvalidArgument' );
		
		var rangeRC = this.getRowCols();
		var baseRC = base.getRowCol();
		var resRC = Object.clone( baseRC );
		
		if (this.type !== 'column') {
			if (baseRC.row == rangeRC.top) resRC.row = rangeRC.bottom;
			else if (baseRC.row == rangeRC.bottom) resRC.row = rangeRC.top;
		}
		
		if (this.type !== 'row') {
			if (baseRC.col == rangeRC.left) resRC.col = rangeRC.right;
			else if (baseRC.col == rangeRC.right) resRC.col = rangeRC.left;
		}
		
		// make the resulting rowcol object array-compatible
		resRC[ 0 ] = resRC.row; resRC[ 1 ] = resRC.col;
		
		return this.getElementHandle( { type: 'cell', rowcol: resRC } );
	}
	
	/** Shifts one or more edges of the range by the given amounts.
	 * @param {cellHandle} anchor a cell which will determine the edges shifted
	 * @param {number} rows the amount to shift the selected row edge
	 * @param {number} cols the amount to shift the selected column edge
	 * @return {cellRange} a derived range with one or two edges shifted
	 */
	, shiftEdges: function (anchor, rows, cols) {
		var anchorRC;
		if (anchor === null) {
			anchorRC = {};
			anchorRC[ 0 ] = anchorRC.row = Number.NaN;
			anchorRC[ 1 ] = anchorRC.col = Number.NaN;
		} else {
			if (!(anchor instanceof cellHandle)) throw new ContextualError(
					'anchor must be a cellHandle',
					'TypeError', 'InvalidArgument' );
			anchorRC = anchor.getRowCol();
		}
		
		/* The current implementation of this method is something of a hack.
		 * A more efficient approach, esp. wrt reuse of cached data, would be
		 * appreciated. This is, however, fast enough for its current use case.
		 */
		
		var rangeRC = Object.clone( this.getRowCols() );
		var touchNW = false, touchSE = false;
		
		if (rows !== 0) {
			if (anchorRC.row == rangeRC.top) {
				touchNW = true;
				rangeRC.top += rows;
			} 
			
			else if (anchorRC.row == rangeRC.bottom) {
				touchSE = true;
				rangeRC.bottom += rows;
			}
			
			else {
				if (rows < 0) {
					touchNW = true;
					rangeRC.top += rows;
				} else {
					touchSE = true;
					rangeRC.bottom += rows;
				}
			}
		}
		
		if (cols !== 0) {
			if (anchorRC.col == rangeRC.left) {
				touchNW = true;
				rangeRC.left += cols;
			}
			
			else if (anchorRC.col == rangeRC.right) {
				touchSE = true;
				rangeRC.right += cols;
			}
			
			else {
				if (cols < 0) {
					touchNW = true;
					rangeRC.left += cols;
				} else {
					touchSE = true;
					rangeRC.right += cols;
				}
			}
		}
		
		if (this.type === 'row') {
			return new cellRange(
					rangeRC.top + 1 + '', rangeRC.bottom + 1 + ''
				);
		}
		
		if (this.type === 'column') {
			return new cellRange(
					toolkit.getAlphaVal( rangeRC.left ),
					toolkit.getAlphaVal( rangeRC.right )
				);
		}
		
		return new cellRange(
				touchNW ? toolkit.formatLocation([
				                rangeRC.top, rangeRC.left ])
				        : cellRange.getElementAddress( this.first ),
				touchSE ? toolkit.formatLocation([
				                rangeRC.bottom, rangeRC.right ])
			            : cellRange.getElementAddress( this.last )
			);
	}
	
	/** Returns a derived range based on the changes to a rowcol object.
	 * @param {object} mod the modified rowcol object
	 * @return {cellRange} a modified range matching the given rowcol object
	 * @private
	 */
	, deriveByBounds: function (mod) {
		/* The current implementation of this method is something of a hack.
		 * A more efficient approach, esp. wrt reuse of cached data, would be
		 * appreciated. This is, however, fast enough for its current use case.
		 */
		
		var orig = this.getRowCols();
		
		var touchNW = mod.top    !== orig.top    || mod.left  !== orig.left;
		var touchSE = mod.bottom !== orig.bottom || mod.right !== orig.right;
		
		if (!touchNW && !touchSE) return this;
		
		if (this.type === 'row') {
			return new cellRange( mod.top + 1 + '', mod.bottom + 1 + '' );
		}
		
		if (this.type === 'column') {
			return new cellRange(
					toolkit.getAlphaVal( mod.left ),
					toolkit.getAlphaVal( mod.right )
				);
		}
		
		return new cellRange(
				touchNW ? toolkit.formatLocation([ mod.top, mod.left ])
				        : cellRange.getElementAddress( this.first ),
				touchSE ? toolkit.formatLocation([ mod.bottom, mod.right ])
			            : cellRange.getElementAddress( this.last ) );
	}
	
	/** Shifts one or more edges of the range to the position of a given cell.
	 * 
	 * @param {cellHandle} anchor a cell on the border of this range which will
	 *        determine the edges shifted in the resulting range
	 * @param {cellHandle} target a cell whose position will determine the
	 *        location of the shifted edges in the resulting range
	 * @return {cellRange} a derived range with one or two edges shifted to
	 *         match the position of the target cell
	 */
	, shiftEdgesToCell: function (anchor, target) {
		if (!(anchor instanceof cellHandle)) throw new ContextualError(
				'anchor must be a cellHandle',
				'TypeError', 'InvalidArgument' );
		if (!(target instanceof cellHandle)) throw new ContextualError(
				'target must be a cellHandle',
				'TypeError', 'InvalidArgument' );
		
		if (anchor.equals( target )) return this;
		
		var rangeRC = Object.clone( this.getRowCols() );
		var anchorRC = anchor.getRowCol();
		var targetRC = target.getRowCol();
		
		if (anchorRC.row == rangeRC.top)
			rangeRC.top = targetRC.row;
		
		else if (anchorRC.row == rangeRC.bottom)
			rangeRC.bottom = targetRC.row;
		
		if (anchorRC.col == rangeRC.left)
			rangeRC.left = targetRC.col;
		
		else if (anchorRC.col == rangeRC.right)
			rangeRC.right = targetRC.col;
		
		var result = this.deriveByBounds( rangeRC );
		if (result === this) throw new ContextualError(
				'anchor not on range boundary', 'InvalidArgument' );
		return result;
	}
	
	/** Shifts the given cell by the given amounts wrapping inside the range.
	 * @param {cellHandle} shift the cell to start at
	 * @param {number} rows the number of rows to shift the cell
	 * @param {number} cols the number of columns to shift the cell
	 * @return {cellHandle} the cell at the requested location
	 */
	, shiftCellWrapped: function (cell, rows, cols) {
		var orig = cell instanceof cellHandle ? cell.getRowCol() : cell;
		var range = this.getRowCols();
		var res = {
			row: orig.row + (rows ? rows : 0)
					+ (orig.rows ? orig.rows : 0)
					+ (orig.carryR ? orig.carryR : 0),
			col: orig.col + (cols ? cols : 0)
					+ (orig.cols ? orig.cols : 0)
					+ (orig.carryC ? orig.carryC : 0)
		};
		
		if (this.type !== 'column') {
			if (res.row > range.bottom) {
				res.rows = res.row - range.bottom - 1;
				if (orig.rows || !orig.carryR) res.carryC = 1;
				res.row = range.top;
			}
			
			else if (res.row < range.top) {
				res.rows = res.row - range.top + 1;
				if (orig.rows || !orig.carryR) res.carryC = -1;
				res.row = range.bottom;
			}
		} else if (res.row < 0) res.row = 0;
		
		if (this.type !== 'row') {
			if (res.col > range.right) {
				res.cols = res.col - range.right - 1;
				if (orig.cols || !orig.carryC) res.carryR = 1;
				res.col = range.left;
			}
			
			else if (res.col < range.left) {
				res.cols = res.col - range.left + 1;
				if (orig.cols || !orig.carryC) res.carryR = -1;
				res.col = range.right;
			}
		} else if (res.col < 0) res.col = 0;
		
		try{
			if (res.rows || res.cols || res.carryR || res.carryC)
				return this.shiftCellWrapped( res );
		}catch(x){
			// too much recursion ... we never want to see this do we?
			
		}
		// make the resulting rowcol object array-compatible
		res[ 0 ] = res.row; res[ 1 ] = res.col;
		if(cell instanceof cellHandle && cell.isMerged()){
				var nextCell = this.getElementHandle( { type: 'cell', rowcol: res } );
				if(nextCell != '<td>undefined</td>'){
					return nextCell;
				}else{
					return this.shiftCellWrapped(cell, rows, ++cols);
				}
		}
		return this.getElementHandle( { type: 'cell', rowcol: res } );
	}
	
	/** Returns a version of this range clipped to the given restrictions.
	 * This operation resembles taking the intersection of two ranges except
	 * that individual bounds of the clipping range may be infinite.
	 * @param {number} firstRow the top-most row, or -1 to not clip
	 * @param {number} firstCol the left-most column, or -1 to not clip
	 * @param {number} lastRow the bottom-most row, or -1 to not clip
	 * @param {number} lastCol the right-most column, or -1 to not clip
	 * @return {cellRange} a new range clipped as requested
	 *         or <code>null</code> if the resulting range would be empty
	 */
	, clipWithin: function (firstRow, firstCol, lastRow, lastCol) {
		var bounds = Object.clone( this.getRowCols() );
		
		if (this.type !== 'column') {
			if (firstRow >= 0 && bounds.top < firstRow)
				bounds.top = firstRow;
			
			if (lastRow >= 0 && bounds.bottom > lastRow)
				bounds.bottom = lastRow;
		}
		
		if (this.type !== 'row') {
			if (firstCol >= 0 && bounds.left < firstCol)
				bounds.left = firstCol;
			
			if (lastCol >= 0 && bounds.right > lastCol)
				bounds.right = lastCol;
		}
		
		// if it clipped to an empty range, return null
		if (bounds.right < bounds.left
				|| bounds.bottom < bounds.top)
			return null;
		
		return this.deriveByBounds( bounds );
	}
	
	, toSheetRange: function() {
		if (this.range.sheets) return this.range.sheets;
		
		this.range.sheets = cellRange.getSheetName( this.firstSheet );
		
		if (this.lastSheet !== this.firstSheet) {
			this.range.sheets += ':'
				+ cellRange.getSheetName( this.lastSheet );
		}
		
		return this.range.sheets;
	}
	
	, toCellRange: function() {
		if (this.range.cells) return this.range.cells;
		
		this.range.cells = cellRange.getElementAddress( this.first );
		
		if (this.last !== this.first)
			this.range.cells += ':' + cellRange.getElementAddress( this.last );
		
		return this.range.cells;
	}
	
	, toString: function() {
		if (this.range.all) return this.range.all;
		this.range.all = this.toSheetRange() + '!' + this.toCellRange();
		return this.range.all;
	}
	
	/**
		Applies formatting to this range of cells.  Updates the grid
		and passes the command on to the workbook in memory
		for style switching  (ie bold on/off, use the first cell in the range as identifier
	**/
	, switchStyle: function(cssIdent, onVal, ajaxPost){
		var val = '';
		var TLCell = this.getCellNW().getCellElement();
		if (cssIdent=='fontWeight'){
			if (TLCell.getStyle('fontWeight')=='bold'){
				val='400';
			}else{
				val='bold';
			}
		}else if (cssIdent=='textDecoration'){
			if (TLCell.getStyle('textDecoration')=='underline'){
				val='none';
			}else{
				val='underline';
			}
		}else if (cssIdent=='fontStyle'){
			if (TLCell.getStyle('fontStyle')=='italic'){
				val='normal';
			}else{
				val='italic';
			}
		}else{ 
			val = onVal;
		}
		if(ajaxPost){
			var url = '/workbook/'+sheet.book.loadby+'/' + sheet.memeId 
					+ '/json/cellrange/setstyle/'
					+ sheet.getSheetName() + '/' + this.toCellRange();
			var _this = this;
			new Ajax.Request(url , {
				method: 'get',
				parameters: {command: cssIdent, value: val},
				onSuccess: function(transport){
					var retObj = transport.responseText.evalJSON();
					sheet.styleHandle.handleStyleResponse(retObj);
					book.setDirty(true);
				},
				onFailure: function(){ 
					try{
						var response = transport.responseText || "failure";     
						parent.showError("Unable to update css: " + response);
					}catch(e){
						parent.showError("Unspecified error updating css.");
					}
				}
			});	
		}
	},
	
	/** Gets the contents of the range as tab-delimeted values.
	 * @return {string} the content of the range with cells separated by
	 *         horizontal tabs and rows seperated by linefeeds
	 */
	getTDV: function(){
		if (this.type !== 'cell') throw new ContextualError(
				'row and column ranges are infinitely large', 'Unsupported' );
		
		return this.inject( "", function (res, cell) {
				var rc = cell.getRowCol();
				if (rc.row !== this.row) {
					if (this.row != -1) res += '\n';
					this.row = rc.row;
				} else {
					res += '\t';
				}
				
				res += cell.getVal();
				
				return res;
			}, { row: -1 } );
	},
	
	/**
		set the border for this cellrange
		
		parameters:
		borderside: surround:top:bottom:left:right
		style: currently unsupported
		color: currently unsupported;
	**/
	setBorder: function(borderside, borderstyle, color){
			var url = '/workbook/' + sheet.book.loadby + '/' + sheet.memeId
					+ '/json/cellrange/setborderstyle/'
					+ sheet.getSheetName() + '/' + this.toCellRange();
			var _this = this;
			var cmd = 'border';
			new Ajax.Request(url , {
				method: 'get',
				parameters: {command: cmd, 
					borderSide: borderside, 
					borderColor: color, 
					borderStyle: borderstyle},
				onSuccess: function(transport){
					var retObj = transport.responseText.evalJSON();
					sheet.styleHandle.handleStyleResponse(retObj);
					book.setDirty(true);
				},
				onFailure: function(){ 
					var response = transport.responseText || "failure";     
					parent.showError("Unable to update css: " + response); 
				}
			});	
			
		editor.closeContextMenus();
	},
	
	/** Makes this range the primary selection.
	 */
	selectIt: function(){
		sheet.getSelection().select( this ).show();
	},

	/** Creates a defined named containg this range;
	 * @param {string} name the name to define
	 */
	createNamedRange: function (name) {
		var val = '';
		var url = '/workbook/' + sheet.book.loadby + '/' + sheet.memeId
			+ '/json/namedrange/createnamedrange/'
			+ sheet.getSheetName() + '/' + this.toCellRange();
		
		new Ajax.Request( url , {
			method: 'GET',
			parameters: { 'rangeName': name },
			
			onSuccess: function (response) {
				var retObj = response.responseText.evalJSON();
				book.insertNamedRange( retObj );
				book.setDirty(true);
			},
			
			onFailure: function (response) {
				var message = "Unable to create named range '" + name + "'";
				uiWindowing.showError( message );
				Logger.httpError( response, message ); 
			}
		});	
	},
	

	/**
		Clears (deletes) the cells in the range
		
		Uses the PluginSheet.fill() method to clear value cells and formats from a range
		
		@param clearCommand pass in:
			1 for clearing formats
			2 for values
			3 for both values and formats
	*/
	clear: function (clearCommand) {
		var what;
		if (1 == clearCommand) what = "formats";
		else if (2 == clearCommand) what = "contents";
		else what = "contents,formats";
		
		var sheet = this.getElementHandle( this.firstSheet );
		
		var url = '/workbook/' + sheet.book.loadby + '/' + sheet.memeId
				+ '/json/cellrange/clear/'
				+ encodeURIComponent( this.toString() );

		new Ajax.Request( url, {
			method: 'GET',
			parameters: { what: what },
			onSuccess: function(transport){
				// update and highlight
				sheet.updateCellsFromJSON( transport.responseText, true );

				// update any charts that may be affected
				sheet.updateCharts();		
				sheet.book.setDirty( true );
			},
			onFailure: function(transport){ 
				Logger.httpError( transport, "Error clearing cells." );
			}
		});
	},
	
	/** Helper function to fetch a data list and re-call {@link #autoFill}.
	 * @param {string} listName the name of the list to fetch
	 * @private
	 */
	_autoFillDataList: function (listName) {	
		/* get list name from first cell, 
		 *  then handle auto-fill
		 *  values from a list
		 */
		var _this = this;
		var url = '/workbook/' + book.loadby + '/' + book.memeId 
			+ '/json/system/getgloballists/' + listName;

		new Ajax.Request( url , {
			method: 'GET',
			
			timeOut: 10,   
			onTimeOut: function() { // waiting 5 sec
				parent.showStatus('still loading...');
			},
			
			onSuccess: function (transport) {
				this.autoFill( null, false, false,
						transport.responseText.evalJSON());
			}.bind( this ),
			
			onFailure: function (response) {
				var message = "error fetching data list";
				uiWindowing.showError( message );
				Logger.httpError( response, message );
			}
		});
	},

	
	
	/** Robo-fills a cell range with values.
	
		Robo-fill is an updated, server-side version of AutoFill.
		
		Becuase Robo-Fill can use the server to process cells before sending to the browser
		the RoboFill is much faster.
		
		Also, RoboFill can handle updating formulas and incrementing row/col references.
	
		Optionally pass in an increment which is multiplied by row# and then appended/added
		
		ie:
			autoFill(1) = 1,2,3
			autoFill(2) = 2,4,6
			etc.
			
			
		TODO: implement list data
		 
	* @param number to increment by 
	* @param {boolean} copyFormats whether to copy formats from initial cell
	*/
	roboFill: function (increment, copyformats) {
		var sheet = this.getElementHandle( this.firstSheet );
		
		var url = '/workbook/' + sheet.book.loadby + '/' + sheet.memeId
				+ '/json/cellrange/fill/'
				+ encodeURIComponent( this.toString() );
		
		var options = {};
		if (increment) options.increment = increment;
		if (copyformats) options.what = "contents,formulas,formats";

		new Ajax.Request( url, {
			method: 'GET',
			parameters: options,
			onSuccess: function (transport) {
				// update and highlight
				sheet.updateCellsFromJSON( transport.responseText, true );

				// update any charts that may be affected
				sheet.updateCharts();		
				sheet.book.setDirty( true );
			},
			onFailure: function (transport) {
				Logger.httpError( transport,
						"Error inserting robofill cells." );
			}
		});
	},
	
	/** Auto-fills a cell range with values.
	
		Optionally pass in an increment which is multiplied by row# and then appended/added
		
		ie:
			autoFill(1) = 1,2,3
			autoFill(2) = 2,4,6
			etc.
			
			
		If a NamedRange is defined as a Global List (aka: a public meme containing lookup lists)
		then it can be used in auto-fill if the first cell contains the name lookup.
		
		ie:
			
			cell a1 = Countries
			     a2 = Albania
			     a3 = Australia
			     a4 = Canada
			     
			     etc.
		 
	* @param number to increment by 
	* @param {boolean} copyFormats whether to copy formats from initial cell
	* @param {boolean} getDataList use data from list named by initial cell
	* @param {object} [listData] the list data with which to fill
	*/
	autoFill: function (increment, copyformats, getDataList, listData) {
		var cx = this.getCellNW();
		var val = cx.getVal();

		if (getDataList) { // auto-fill with list data
			this._autoFillDataList(val);
		} else {
			// set the vals
			var cells = this.getCells();
			for(var t=0;t<cells.length;t++){
			
				try{ // copy formats... TODO: use persistent ajax methods
					cells[t].cell.className=cx.cell.className; // css id
				}catch(e){;}
				
				// listvals should be a JSONArray at this point
				if (listData) { // we have a list
					
					if(listData.vals == null){
						parent.showStatus("Data list not found.<br/>Use any named range in a public doc.");
						return;
					}
					var v = listData.vals[ t ];
					if (v) cells[t].setVal( v );
				} else if((increment!=null)&&
					(increment != '')){
					
					var tz = increment * t;
					var vx = new Number(val);
					if(!isNaN(vx)){
						vx += tz;
						cells[t].setVal(vx);
					}else if(val.indexOf('=')==0){ /// a formula, add this to the value						
						cells[t].setVal(val+ '+' + tz);
					}else{
						cells[t].setVal(val+ ' ' + tz);
					}
				}else{
					cells[t].setVal(val);
				}
			}
		}
				
		// hide the menu item
		$('roboFillChoiceMenu').toggle();
		
	},
	
	/**
		create an auto-formula
	*/
	autoFormula: function (type) {
		var formula;
		if (type.charAt( 0 ) === '=') {
			// we already have a complete formula, just use it
			formula = type;
		} else {
			formula = '=' + type.toUpperCase()
					+ '(' + this.toCellRange() + ')';
		}

		var rc = cellRange.getElementRowCol( this.last );
		rc[ 0 ] = rc.row = rc.row + 1;
		
		var cell = this.getElementHandle({ type: 'cell', rowcol: rc });
		cell.setVal( formula );	
		
		// hide the menu item
		try{
			$('autoFormulaChoiceMenu').toggle();
		} catch (e) {}
		
		sheet.getSelection().select( this.shiftEdges( null, 1, 0 ) ).show();
	},

	/** toggles merging of the cells in this cell range
	**/
	merge: function(){
		if (this.type !== 'cell')
			throw new ContextualError(
					'only cell ranges may be merged', 'Unsupported' )
				.add( 'range', this );
		
		var cell = this.getFirst();
		var url = '/workbook/' +sheet.book.loadby + '/' + sheet.memeId
				+ '/json/cellrange/' 
				+ (cell.isMerged() ? 'unmerge' : 'merge') + '/'
				+ sheet.getSheetName() + '/' + this.toCellRange();
		
		var _this = this;
		var _sheet = sheet;
		new Ajax.Request(url , {
			method: 'get',
			onSuccess: function(transport){
				var retObj = transport.responseText.evalJSON();
				var cell = _sheet.getCell(retObj.loc);
				cell.updateInternal(retObj);
				book.setDirty(true);
				parent.showStatus("merged: " + retObj.loc);
			},
			onFailure: function(){ 
				var response = transport.responseText || "failure";     
				parent.showError("Unable to merge cellrange " +this.cellRange +': '+ response); 
			}
		});	
		
	},
	
	/**
	 * Returns an array representing the rows and 
	 * columns that this cellrange encompasses.  This is an int array
	 * and handles out of order cell range selections
	 * [topLeftCellRow][topLeftCellCol][bottomRightCellRow][bottomRightCellCol]
	 * 
	 */
	getRowCols: function(){
		if (this.rowcol) return this.rowcol;
		var rc = {};
		
		if (this.type === 'row') {
			rc[ 0 ] = rc.top    = cellRange.getElementIndex( this.first );
			rc[ 1 ] = rc.left   = 0;
			rc[ 2 ] = rc.bottom = cellRange.getElementIndex( this.last );
			rc[ 3 ] = rc.right  = Number.POSITIVE_INFINITY;
		}
		
		else if (this.type === 'column') {
			rc[ 0 ] = rc.top    = 0;
			rc[ 1 ] = rc.left   = cellRange.getElementIndex( this.first );
			rc[ 2 ] = rc.bottom = Number.POSITIVE_INFINITY;
			rc[ 3 ] = rc.right  = cellRange.getElementIndex( this.last );
		}
		
		else {
			var rc1 = cellRange.getElementRowCol( this.first );
			var rc2 = cellRange.getElementRowCol( this.last );
			
			rc[ 0 ] = rc.top    = rc1.row;
			rc[ 1 ] = rc.left   = rc1.col;
			rc[ 2 ] = rc.bottom = rc2.row;
			rc[ 3 ] = rc.right  = rc2.col;
		}
		
		return this.rowcol = rc;
	},
	
	/**
	 * Return the dimensions of this cell range (in cells) specified
	 * as an int array [rows][cols]
	 * 
	 * That is, A1:B6 would be [2][6]
	 */
	getDimensions: function(){
		if (this.dimensions) return this.dimensions;
		var rc = this.getRowCols();
		
		var dim = this.dimensions = {};
		dim[ 0 ] = dim.height = rc.bottom - rc.top + 1;
		dim[ 1 ] = dim.width  = rc.right - rc.left + 1;
		
		return this.dimensions;
	},
	
	_each: function (iter) {
		if (this.type === 'cell') {
			var first = cellRange.getElementRowCol( this.first );
			var last  = cellRange.getElementRowCol( this.last );
			for (var row = first.row; row <= last.row; row++) {
				for (var col = first.col; col <= last.col; col++) {
					iter( this.getElementHandle({
							type: this.type, rowcol: [ row, col ] }) );
				}
			}
		}
		
		else {
			var first = cellRange.getElementIndex( this.first );
			var last  = cellRange.getElementIndex( this.last );
			for (var idx = first; idx <= last; idx++) {
				iter( this.getElementHandle({ type: this.type, index: idx }) );
			}
		}
	},
	
	/**
	 * returns an array of rows, each containing an array of cells that
	 * are encompassed by this cellrange
	 * 
	 */
	getCellsInRows: function(){
		var rows = new Array();
		var rowcol = this.getRowCols();
		if(sheet==null)
			sheet=book.getSelectedSheet();
		for (var i=rowcol[1]; i<=rowcol[3];i++){
			var row = new Array();
			for (var x=rowcol[0];x<=rowcol[2];x++){
				row[row.length] = sheet.getCell(toolkit.getAlphaVal( i )+(x+1));
			}
			rows[rows.length]= row;
		}	
		return rows;	
	},
		

	/**
		returns an array of cells which the cellRange spans
	**/
    getCells: function(){
	    this.componentCells = new Array();
	    var rowcol = this.getRowCols();
		if(sheet==null)
			sheet=book.getSelectedSheet();
		for (var i=rowcol[1]; i<=rowcol[3];i++){
			for (var x=rowcol[0];x<=rowcol[2];x++){
				this.componentCells[this.componentCells.length] = sheet.getCell(toolkit.getAlphaVal( i )+(x+1));
			}
		}	
		return this.componentCells;
    }
	
	/** Creates and returns a handle to the element.
	 * @param {object} an element object
	 * @return {cellHandle|colHandle|rowHandle} a handle to the element
	 * @private
	 */
	, getElementHandle: function(element) {
		if (element.handle) return element.handle;
		
		if (this.lastSheet !== this.firstSheet)
			throw new ContextualError(
					'cannot create element handles for multi-sheet ranges',
					'Unsupported' );
		
		if (this.firstSheet.handle
				? this.firstSheet.handle !== sheet
				: this.firstSheet.name !== sheet.getSheetName() )
			throw new ContextualError(
					'cannot create handles for elements off the current sheet',
					'Unsupported' );
		
		if (cellRange.getElementType( element ) === 'cell') {
			element.handle = sheet.getCell(
					cellRange.getElementAddress( element ) );
		}
		
		else if (element.type === 'row') {
			element.handle = new rowHandle(
					cellRange.getElementIndex( element ) );
		}
		
		else if (element.type === 'column') {
			element.handle = new colHandle(
					cellRange.getElementAddress( element ) );
		}
		
		return element.handle;
	}
});

/**
 * @private
 */
cellRange.splitSheets = function (range) {
	var result = { all: range };
	
	var bang = range.indexOf( '!' );
	if (bang !== -1) {
		result.sheets = range.substring( 0, bang );
	}
	
	result.cells = range.substring( bang + 1 );
	
	// temporary hack to deal with input from Excel
	result.cells = result.cells.replace( /\$/g, '' );
	
	return result;
};

/**
 * @private
 */
cellRange.splitRange = function (range) {
	var result = {};
	
	var split = range.indexOf( ':' );
	if (split !== -1) {
		result.first = range.substring( 0, split );
		result.last = range.substring( split + 1 );
		
		var splitCheck = range.indexOf( ':', split + 1 );
		if (splitCheck !== -1) throw new ContextualError(
				'spurious delimeter', 'SyntaxError' )
				.add( 'token', ':' )
				.add( 'column', splitCheck );
	} else {
		result.first = range;
	}
	
	return result;
};

/** Checks whether the given value is an element handle object.
 * @param object the value to be checked
 * @return {boolean} whether the given value is an element handle object
 * @private
 */
cellRange.isElementHandle = function (object) {
	return object instanceof cellHandle
			|| object instanceof rowHandle
			|| object instanceof colHandle;
};

/** Calculates and returns the type of the given element.
 * @param {object} an element object
 * @return {string} one of <code>'cell'</code>, <code>'column'</code>,
 *         or <code>'row'</code>
 * @private
 */
cellRange.getElementType = function (element) {
	if (element.type) return element.type;
	
	if (element.handle) {
		if (element.handle instanceof cellHandle)
			element.type = 'cell';
		else if (element.handle instanceof rowHandle)
			element.type = 'row';
		else if (element.handle instanceof colHandle)
			element.type = 'column';
		else throw new ContextualError(
				'unknown handle class while determining element type',
				'TypeError' )
				.add( 'value', element.handle )
				.add( 'element', element );
	}
	
	else if (element.addr)
		element.type = toolkit.getAddressType( element.addr );
	
	else throw new ContextualError(
			'no basis on which to determine element type' )
			.add( 'element', element );
			
	return element.type;
};

/** Finds and returns the element's Excel-style address string.
 * @param {object} an element object
 * @return {string} the element's Excel-style address
 * @private
 */
cellRange.getElementAddress = function (element) {
	if (element.addr) return element.addr;
	
	if (typeof element.handle !== 'undefined')
		element.addr = element.handle.getAddress();
	
	else if (typeof element.rowcol !== 'undefined')
		element.addr = toolkit.formatLocation( element.rowcol );
	
	else if (typeof element.index !== 'undefined') {
		if (cellRange.getElementType( element ) === 'column')
			element.addr = toolkit.getAlphaVal( element.index );
		else element.addr = element.index + 1 + '';
	}
	
	else throw new ContextualError(
			'no basis on which to determine element address' )
			.add( 'element', element );
	
	return element.addr;
};

/** Finds and returns the zero-based row-column address of the given element.
 * @param {object} an element object
 * @return {object} an object/array hybrid as returned by
 *         {@link gridToolkit#getRowColFromString}
 * @private
 */
cellRange.getElementRowCol = function (element) {
	if (element.rowcol) return element.rowcol;
	
	if (cellRange.getElementType( element ) !== 'cell')
		throw new ContextualError( 'only cells have row-col addresses' );
	
	if (element.handle)
		element.rowcol = element.handle.getRowCol();
	
	else if (element.addr)
		element.rowcol = toolkit.getRowColFromString( element.addr );
	
	else throw new ContextualError(
			'no basis on which to determine element row-col address' )
			.add( 'element', element );
	
	return element.rowcol;
};

/** Finds and returns the zero-based numeric address of the given element.
 * @param {object} an element object
 * @return {number} the element's zero-based numeric address
 * @private
 */
cellRange.getElementIndex = function (element) {
	if (typeof element.index === 'number') return element.index;
	
	if (cellRange.getElementType( element ) === 'cell')
		throw new ContextualError( "cells don't have indices" );
	
	if (element.handle)
		element.index = element.handle.getIndex();
	
	else if (element.addr) {
		if (element.type === 'column')
			element.index = toolkit.getIntVal( element.addr );
		else element.index = element.addr - 1;
	}
	
	else throw new ContextualError(
			'no basis on which to determine element index' )
			.add( 'element', element );
	
	return element.index;
};

/** Finds and returns the name of the given sheet.
 * @param {object} a sheet object
 * @return {string} the sheet's name
 * @private
 */
cellRange.getSheetName = function (sheet) {
	if (sheet.name) return sheet.name;
	
	if (sheet.handle)
		sheet.name = sheet.handle.getSheetName();
	
	else throw new ContextualError(
			'no basis on which to determine sheet name' )
			.add( 'sheet', sheet );
	
	return sheet.name;
};
