/* Copyright (c) 2008 Extentech Inc. All Rights Reserved

##### Sheetster&trade; Web Application #####

This file is a part of the Sheetster&trade; 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 from Extentech.

For a fully supported and redistributable commercial license, 
please visit www.extentech.com or contact us at:

 sales@extentech.com
 Extentech Inc.
 1032 Irving Street #910
 San Francisco, CA 94122
 415-759-5292
 
*/
/**
	gridToolkit is a collection of general utilities for the spreadsheet.
	
	Think of them as static utility methods in java.

	@class gridToolkit general utilities for the spreadsheet.
*/
gridToolkit = Class.create();
gridToolkit.prototype = {
	
	/**
	 * setup css rule cache
	 */	
	initialize: function() {
		this.rulescache = new Array();
	},
	
/**
	Get the active sheet that is being worked on.  Note that this class could
	be instantiated either in the grid iframe, where we have a 'sheet' variable
	available, or outside of the iframe where we could have multiple sheets.
	
	deprecated, moving to UIMgr
**/
	getActiveSheet: function(){
		var framedoc = uiWindowing.getActiveDoc();
		if(framedoc.sheet == null){
			showStatus("Please select an active sheet.");
			return null;
		}
		return framedoc.sheet;
	},
	
	/**
		Takes a response from an ajax requests and identifies if it is a json
		error message being returned.  If so, show the error and return true.
	**/
	isErrorMessage: function(errorJson){
		try{
			jsonResponse = errorJson.evalJSON();
			if (jsonResponse.errormessage != null){
				parent.showError(jsonResponse.errormessage);
				return true;
			}
		}catch(ex){
			return false;
		}
		return false;
	},
	
	/**
		gets a named rule from the attached stylesheet
	
		caches the rules... the iterating kills performance
	
		Optional stylesheet number restricts the search for performance.
		Our automatic additions to the stylesheet are all in 0 currently
	**/
	getExternalRule: function(ruleTitle, ssNumber){
		
		// try the cache first otherwise build
		var rx = this.rulescache[ruleTitle];
		if(rx!=null){
			// alert('gridToolkit.getExternalRule() cache hit');
			return rx;
		}
		
		var i, x, sht, rules;
		if(ssNumber!=null){
			sht = document.styleSheets[ssNumber];
			rules = sht.cssRules || sht.rules;
			for (i = rules.length - 1; 0 <= i; i--){
				// always refresh the cache
				this.rulescache[rules[i].selectorText] = rules[i];
				
				if (rules[i].selectorText == ruleTitle){
					return rules[i];
				}
			}
		}else{
			for (x = 0; x < document.styleSheets.length; x++){
				sht = document.styleSheets[x];
				rules = sht.cssRules || sht.rules;
				
				for (i = rules.length - 1; i >= 0; i--){
					this.rulescache[rules[i].selectorText] = rules[i];
					
					if (rules[i].selectorText == ruleTitle){
						return rules[i];
					}
				}
			}
		}
		return null;
	},
	
	/**
		set a rule element, 
		
		This is far more efficent for setting multiple styles on an element  than
		the utlity method, changeCssRule, as it only needs to iterate the rules in the stylesheet
		once
		
		rule: an actual css rule element, get from getExternalRule()
		styles 
		
		toolkit.setExternalRule(theRule, {width: '23px'});
		($ does not work for external stylesheet rules, it will affect the wrong node)
	**/
	setExternalRule: function(rule, styles){
		if (rule != null){ 
			for (var property in styles){
				rule.style[property] = styles[property];
			}
			return true;
		}
		return false;
	},
	
	/**
	 * Change a css rule, if the rule does not exist it will be 
	 * created within the default [0] stylesheet
	 * 
	 * ruleident: the identity of the rule ".class"
	 * rule: the rule to change "border-bottom"
	 * value: new value of the rule "2px"
	 */
	changeCssRule: function(ruleident, rule, value){
		var cRule = this.getExternalRule(ruleident , 0);
		var ss = document.styleSheets[0];
		if (cRule==null){
			if (document.all&&document.getElementById){ // IE sillyness
					ss.addRule(ruleident, rule +':' + value + ';', 0);
			}else{
					ss.insertRule(ruleident + "{" + rule + ':' + value + ';' + "}", 0);
			}
		}else{
			 if(cRule.style[rule]){
				 cRule.style[rule] = value;
		     }
		}
	},

	/** Updates the style number class associated with the given elements.
	 * @param {Element} source the element whose style class should be copied
	 * @param {Element} targets... the elements whose style class should be changed
	 * @private
	 */
	copyStyleClass: function (source) {
		var classes = (source.className.match( /(^|\s+)s\d+/g ) || []).join( '' );
		
		for (var idx = 1; idx < arguments.length; idx++) {
			var target = arguments[ idx ];
			target.className = 
				target.className.replace( /(^|\s+)s\d+/g, '' ) + ' ' + classes;
		}
	},
	
	/**
	 *  Unfortunately, we cannot use CSS to set the width of the data content div to 100%
	 *  because it destroys ability to resize cols.
	 *  
	 *  So we call this to explicitly set the width when needed.
	 * 
		Set the internal width of the data div in the cell to the width
		of the containing cell.  5 pixels are removed for padding, otherwise
		the grid can be thrown off between panes.
	**/
	setFixedWidth: function(cellDiv){
		cellDiv.style.width=(cellDiv.parentNode.clientWidth-5) + 'px';
	},
	
	/**
		Remove width constraints on the internal data div.
	**/
	removeFixedWidth: function(cellDiv){
			cellDiv.setAttribute('style', '');
	},
	
	/**
		Set the internal height of the data div in the cell to the height
		of the containing cell.
	**/
	setFixedHeight: function(cellDiv){
		cellDiv.setStyle({
			height: (cellDiv.parentNode.getHeight()-1) + 'px'
		});
	},
	
	/**
		Remove height constraints on the internal data div.
	**/
	removeFixedHeight: function(cellDiv){
			cellDiv.setAttribute('style', '');
	},

	/**
		Takes a response from an ajax requests and identifies if it is a json
		error message being returned.  If so, show the error and return true.
	**/
	isErrorMessage: function(errorJson){
		try{
			jsonResponse = errorJson.evalJSON();
			if (jsonResponse.errormessage != null){
				parent.showError(jsonResponse.errormessage);
				return true;
			}
		}catch(ex){
			return false;
		}
		return false;
	},

	/** Converts a numeric column address to its alphabetic equivalent.
	 * @param {number} value the zero-based numeric column address to convert
	 * @throws TypeError if the given value is not a number
	 * @throws RangeError if the given value is less than zero
	 */
	getAlphaVal: function (value) {
		if (typeof value === 'string') value = Number( value );
		else if (typeof value !== 'number')
			throw new TypeError( 'the input value must be a number' );
		if (!isFinite( value ) || value < 0 || Math.floor( value ) !== value)
			throw new RangeError(
				'the input value must be zero or a finite positive integer' );
		
		var result = '';
		while (value >= 0) {
			// 65 is the Unicode code point for 'A' (LATIN CAPITAL LETTER A)
			result = String.fromCharCode( value % 26 + 65 ) + result;
			value = Math.floor( value / 26 ) - 1;
		}
		
		return result;
	},
	
	/** Converts an alphabetic column address to its numeric equivalent.
	 * @param {string} value the alphabetic column address to convert
	 * @throws TypeError if the given value is not a string
	 * @throws RangeError if the given value contains non-alphabetic characters
	 */
	getIntVal: function (value) {
		if (typeof value !== 'string')
			throw new TypeError( 'the input value must be a string' );
		
		// allow input in lower case
		value = value.toUpperCase();
		
		var result = -1;
		for (var idx = 0; idx < value.length; idx++) {
			// 65 is the Unicode code point for 'A' (LATIN CAPITAL LETTER A)
			var digit = value.charCodeAt( idx ) - 65;
			if (digit < 0 || digit > 25) throw new RangeError(
					'the input value may contain only alphabetic characters' );
			
			result = (result + 1) * 26 + digit;
		}
		
		return result;
	},
	
	/** Converts a numeric cell address to an Excel-style string.
	 * @param {array|object} address the address to be converted
	 * @param {number} address.0 the zero-based numeric row address
	 * @param {number} address.1 the zero-based numeric column address
	 * @return {string} the Excel style equivalent of the given address
	 * @throws TypeError if either address is not a number
	 * @throws RangeError if either address is less than zero
	 */
	formatLocation: function (address) {
		if (typeof address !== 'array' && typeof address !== 'object')
			throw new TypeError( 'the input must be an array or object' );
		if (typeof address[ 0 ] !== 'number')
			throw new TypeError( 'the row address must be a number' );
		if (!isFinite( address[ 0 ] ) || address[ 0 ] < 0)
			throw new RangeError(
					'addresses must be zero or a positive finite integer' );
		
		return this.getAlphaVal( address[ 1 ] ) + (address[ 0 ] + 1);
	},

	/** Parses a cell address into an object/array hybrid.
	 *
	 * @param {string} address the Excel-style cell address to parse
	 * @return {object} an object/array hybrid. The zero-based row number is
	 *         at index 0 and the <code>row</code> property. The zero-based
	 *         column number is at index 1 and the <code>col</code> property.
	 */
	getRowColFromString: function (address) {
		if (typeof address !== 'string')
			throw new TypeError( 'the input value must be a string but was a ' + typeof address );
		
		var parts = address.match( /^\$?([A-Za-z]+)\$?(\d+)$/ );
		if (!parts) throw new SyntaxError(
				'the input value is not a valid Excel-style address' );
		
		var col = this.getIntVal( parts[ 1 ] );
		var row = parts[ 2 ] - 1;

		return {
			  0: row
			, 1: col
			, row: row
			, col: col
		};
	},

	/** Shifts the given address by the specified amount.
	 * The base address may be given as a <code>cellHandle</code> object, a
	 * rowcol array or array/object hybrid, or an Excel-style address string.
	 *  
	 * @param {cellHandle|array|string} base the cell from which to calculate
	 * @param {number} rows the number of rows to add
	 * @param {number} cols the number of columns to add
	 * @return {string} the resulting address as an Excel-style string
	 * @throws {RangeError} if the resulting address is invalid
	 */
	shiftAddress: function (base, rows, cols) {
		var rc;
		
		if (base instanceof cellHandle)
			rc = base.getRowCol();
		else if (typeof base === 'string')
			rc = this.rowColFromString( base );
		else rc = base;
		
		return this.formatLocation([ rc[0] + rows, rc[1] + cols ]);
	},
	
	/** Determines the element type of the given address.
	 * @param {string} address the address to check
	 * @return {string} one of <code>'cell'</code>, <code>'column'</code>,
	 *         or <code>'row'</code>
	 */
	getAddressType: function (address) {
		if (typeof address !== 'string') throw new ContextualError(
				'address must be a string', 'TypeError', 'IllegalArgument' )
				.add( 'value', address );
		if (address === '') throw new ContextualError(
				'address must not be the empty string', 'IllegalArgument' ); 
		
		var match = address.match( /^(\$?[A-Za-z]*)(\$?\d*)$/ );
		if (match === null) throw new ContextualError(
				'invalid address', 'SyntaxError' )
				.add( 'value', address );
		
		if (match[ 1 ] === '') return 'row';
		if (match[ 2 ] === '') return 'column';
		return 'cell';
	},

	/**
		takes a cell (ie C4) and adjusts its id so it becomes C5.
		also handles moving the internal
	**/
	incrementCellRowNum: function(theCell){
		var rc = this.getRowColFromString(theCell.getAttribute('id'));
		if (rc!=null){
			rc[0] += 1;
			var nAddr = toolkit.formatLocation(rc);  
			theCell.setAttribute('id', nAddr);
		}
	},
	
	
	/**
		takes a cell (ie C4) and adjusts its id so it becomes D4.
	**/
	incrementCellColNum: function(theCell){
		var rc = this.getRowColFromString(theCell.getAttribute('id'));
		if (rc!=null){
			rc[1] += 1;
			var nAddr = toolkit.formatLocation(rc);  
			theCell.setAttribute('id', nAddr);
		}
	},

	/**
		takes a cell (ie C4) and adjusts its id so it becomes C3.
	**/
	decrementCellRowNum: function(theCell){
		var rc = this.getRowColFromString(theCell.getAttribute('id'));
		if (rc!=null){
			rc[0] -= 1;
			var nAddr = toolkit.formatLocation(rc);  
			theCell.setAttribute('id', nAddr);
		}
	},

	/**
		takes a cell (ie C4) and adjusts its id so it becomes C3.
	**/
	decrementCellColNum: function(theCell){
		var rc = this.getRowColFromString(theCell.getAttribute('id'));
		if (rc!=null){
			rc[1] -= 1;
			var nAddr = toolkit.formatLocation(rc);  
			theCell.setAttribute('id', nAddr);
		}
	},
	
	/** Returns the target element from an event.
	 * @deprecated Use Prototype's <code>Event.element()</code> instead.
	 */
	getEventTarget: function(evt) {
	       evt = (evt) ? evt : ((window.event) ? window.event : null);
		   if (evt) {
				var target = (evt.target) ? evt.target : evt.srcElement;
				if (target) {
				 		 return Element.extend(target);			 		
				}
		 	}
		 	return null;
	},
	
	/**
		Deletes a css rule from the collection of rules
		
		returns true if rule found, false if rule not found
	**/
	deleteCSSRule: function(ruleName){
		ruleName = ruleName.toLowerCase();
		 for (var i=0; i<document.styleSheets.length; i++) { 
	         var styleSheet=document.styleSheets[i];         
	         var ii=0;                                       
	         var cssRule=false;                              
	         do {                                            
	            if (styleSheet.cssRules) {         
	               cssRule = styleSheet.cssRules[ii];         
	            } else {                                    
	               cssRule = styleSheet.rules[ii];          
	            }                                          
	            if (cssRule)  {                              
	            	var currName = cssRule.selectorText.toLowerCase();
	            	currName = currName.substring(currName.indexOf('.'), currName.length);
	               if (currName==ruleName) { 
	                     if (styleSheet.cssRules) {       
	                        styleSheet.deleteRule(ii);      
	                     } else {                          
	                        styleSheet.removeRule(ii);    
	                     }                                  
	                     return true;                                    
	               }                                        
	            }                                           
	            ii++;                                   
	         } while (cssRule)                              
	      }
	      return false;                              
	},
	
	/**
		Greys out the screen
	**/
	greyOut: function(trueFalse) {
		
		if(is.iphone || is.ipad) // do not do for iphone...
			return;
			
	  	var dark=$('darkenScreen');
		if (trueFalse) {
    		var msx = "<br><br><br><br><br><br><br><br><br><br><div class='title' align='center'>Loading...<br/><img src='/themes/images/loading.gif' align='absmiddle'></div>";
			dark.innerHTML = msx;
	    	dark.show();                          
	  	} else {
    		dark.hide();
  		}
	},
	
	/** Fires a copy of the given mouse event on the given element.
	 * This is useful for proxying events onto a different element.
	 * 
	 * @param {Element} target the element on which to fire the copied event
	 * @param {MouseEvent} event the event to be copied
	 */
	refireMouseEvent: function (target, event) {
		// W3C DOM Events
		if (typeof target.dispatchEvent === 'function') {
			var result = document.createEvent( 'MouseEvents' );
			result.initMouseEvent(
					  event.type // event type
					, true // whether the event can bubble
					, true // whether the event is cancelable
					, event.view // ?
					, event.detail // the number of times the user has clicked
					, event.screenX, event.screenY
					, event.clientX, event.clientY
					, event.ctrlKey, event.altKey
					, event.shiftKey, event.metaKey
					, event.button // which mouse button was pressed
					, event.relatedTarget
				);
			target.dispatchEvent( result );
		}
	
		// Internet Explorer 8 and below
		else if (typeof target.fireEvent !== 'undefined') {
			var result = document.createEventObject( event );
			target.fireEvent( 'on' + event.type, result );
		}
	}
};

/// End of prototype object


function callInProgress (xmlhttp) {
		switch (xmlhttp.readyState) {
		case 1: case 2: case 3:
		return true;
		break;
		// Case 4 and 0
		default:
		return false;
		break;
	}
}
function showFailureMessage() {
	parent.showError("A communications error with the server occurred.  Please save your work and restart the worksheet");
}


