/**
 * REST.js
 * 
 * Copyright (c) 2005 Federico Fissore (federico AT fsfe.org), Bill Pierce (wcpierce AT gmail.com)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in all copies or 
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Based on an article from Bill Pierce
 * http://www.codeproject.com/aspnet/AJAXWasHere-Part1.asp
 * and following comments
 */

function REST(cMethod, cAsync, cCachePolicy, cDebugTextAreaName) {

	//constants
	var AVOID_CACHE_PARAM_NAME = 'avoid_ugly_cache';

	//cache
	var cachePolicy;
	var cache;

	//XMLHttpRequest
	var method;
	var isAsync;
	var hasXMLHttpRequest;
	var xmlHttp;
	var dummyThis;

	/**
	 * create an instance of the "right" XMLHttpRequest object (depending on the browser)
	 */
	function getXMLHttpRequest(pURL, pData) {
		cacheURL = createQueryString(pURL, pData);
		if (cache.containsKey(cacheURL)) {
			return cache.get(cacheURL);
		} else if (hasXMLHttpRequest) {
			try {
				return new XMLHttpRequest();
			} catch (e) {
				return false;
			}
		} else {
			return new ActiveXObject('Microsoft.XmlHttp');
		}
	}

	/**
	 * handles readyState changes when calling URLs asyncronously
	 */
	function readyStateChange(RESTInstance, pURL, pData) {
		if (xmlHttp.readyState == 1) {
			RESTInstance.onLoading(pData);
		} else if (xmlHttp.readyState == 2) {
			RESTInstance.onLoaded(pData);
		} else if (xmlHttp.readyState == 3) {
			RESTInstance.onInteractive(pData);
		} else if (xmlHttp.readyState == 4) {
			if (xmlHttp.status == 0) {
				RESTInstance.onAbort(pData);
			} else if (xmlHttp.status == 200 && xmlHttp.statusText == 'OK') {
				RESTInstance.getCache().put(pURL, xmlHttp);
				RESTInstance.onComplete(xmlHttp.responseText, xmlHttp.responseXML, pData);
			} else {
				RESTInstance.onError(xmlHttp.status, xmlHttp.statusText, xmlHttp.responseText, pData);
			}
		}
	}

	/**
	 * from a string or from an existent Parameters instance, a new Parameters instance is returned
	 */
	function createParameters(pData) {
		if (typeof pData != 'string') {
			return new Parameters(pData.toString()); //this is a hack to clone the Parameters object
		} else if (pData != null) {
			return new Parameters(pData);
		}
	}

	/**
	 * creates the querystring from the url and the parameters
	 */
	function createQueryString(pURL, pData) {
		pURL = '' + pURL;
		return pURL + (pURL.indexOf('?') >= 0 ? '&' : '?') + pData.toString();
	}

	/**
	 * gets a random number
	 */
	function getRandom() {
		return Math.round(Math.random()*1000000000);
	}

	/**
	 * starts a request trying to avoid external cache (browser, proxies...)
	 */
	function sendAvoidCache(pURL, pData, pCleanData) {
		pData.put(AVOID_CACHE_PARAM_NAME, getRandom());
		sendIgnoreCache(pURL, pData, pCleanData);
	}

	/**
	 * returns an internally saved request response: if the request has never been requested, starts a plain request
	 */
	function sendFromCache(pURL, pData, pCleanData) {
		cacheURL = createQueryString(pURL, pData);
		if (cache.containsKey(cacheURL)) {
			if (isAsync) {
				dummyThis.onComplete(xmlHttp.responseText, xmlHttp.responseXML, pData); //manually fires the event
			}
		} else {
			sendIgnoreCache(pURL, pData, pCleanData);
		}
	}

	/**
	 * starts a request the plain way
	 */
	function sendIgnoreCache(pURL, pData, pCleanData) {
		if (xmlHttp && (xmlHttp.readyState == 4 || xmlHttp.readyState == 0)) {
			var cacheURL = createQueryString(pURL, pCleanData);

			if (method == 'get') {
				pURL = createQueryString(pURL, pData);
			}

			xmlHttp.open(method, pURL, isAsync);

			xmlHttp.onreadystatechange = function() {
				readyStateChange(dummyThis, cacheURL, pData);
			};

			if (method == 'get') {
				if (hasXMLHttpRequest) {
					xmlHttp.send(null);
				} else {
					xmlHttp.send();
				}
			} else {
				xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
				xmlHttp.send(pData.toString());
			}

			if (!isAsync) {
				cache.put(cacheURL, xmlHttp);
			}
		}
	}

	/**
	 * choose the right request type, depending on the choosen cache policy, and starts it
	 */
	this.send = function(pURL, pData) {
		var params = createParameters(pData);
		params.remove(AVOID_CACHE_PARAM_NAME); //get away the avoid cache param (if any)
		var cleanParams = createParameters(params);

		xmlHttp = getXMLHttpRequest(pURL, params);

		if (method == 'get') {
			switch (cachePolicy) {
				case CachePolicy.AVOID:
					sendAvoidCache(pURL, params, cleanParams);
					break;
				case CachePolicy.USE_INTERNAL:
					sendFromCache(pURL, params, cleanParams);
					break;
				default:
					sendIgnoreCache(pURL, params, cleanParams);
					break;
			}
		} else {
			sendIgnoreCache(pURL, params, cleanParams);
		}
	};

	/**
	 * returns the cache
	 */
	this.getCache = function() {
		return cache;
	};

	/**
	 * sets the request method
	 */
	this.setMethod = function(pMethod) {
		method = pMethod.toLowerCase();
		if (method != 'post' && method != 'get') {
			alert('Invalid method! ' + method);
			return false;
		}
	};

	/**
	 * sets the cache policy
	 */
	this.setCachePolicy = function(pCachePolicy) {
		cachePolicy = pCachePolicy.toLowerCase();
		if (cachePolicy != CachePolicy.AVOID && cachePolicy != CachePolicy.USE_INTERNAL && cachePolicy != CachePolicy.IGNORE) {
			alert('Invalid cache policy! ' + cachePolicy);
			return false;
		}
	};

	/**
	 * gets the request method
	 */
	this.getMethod = function() {
		return method;
	};

	/**
	 * gets the cache policy
	 */
	this.getCachePolicy = function() {
		return cachePolicy;
	};

	/**
	 * sets the async flag
	 */
	this.setAsync = function(pAsync) {
		isAsync = pAsync;
	};

	/**
	 * gets the async flag
	 */
	this.isAsync = function() {
		return isAsync;
	};

	/**
	 * abort the request
	 */
	this.abort = function() {
		if (xmlHttp) {
			xmlHttp.abort();
		}
	};

	/**
	 * gets the request response (text version)
	 */
	this.getResponseText = function() {
		if (xmlHttp) {
			return xmlHttp.responseText;
		}
	};

	/**
	 * gets the request response (XML version, only if available)
	 */
	this.getResponseXML = function() {
		if (xmlHttp) {
			return xmlHttp.responseXML;
		}
	};

	/**
	 * gets the request ready state
	 */
	this.getReadyState = function() {
		if (xmlHttp) {
			return xmlHttp.readyState;
		}
	};

	/**
	 * set the request status (numeric)
	 */
	this.getStatus = function() {
		if (xmlHttp) {
			return xmlHttp.status;
		}
	};

	/**
	 * set the request status (string)
	 */
	this.getStatusText = function() {
		if (xmlHttp) {
			return xmlHttp.statusText;
		}
	};

	/**
	 * event fired when request is async and readyState is onLoading
	 */
	this.onLoading = function(parameters) {
		// Loading
	};

	/**
	 * event fired when request is async and readyState is onLoaded
	 */
	this.onLoaded = function(parameters) {
		// Loaded
	};

	/**
	 * event fired when request is async and readyState is onInteractive
	 */
	this.onInteractive = function(parameters) {
		// Interactive
	};

	/**
	 * event fired when request is async and readyState is onComplete
	 */
	this.onComplete = function(responseText, responseXml, parameters) {
		// Complete
	};

	/**
	 * event fired when request is async and readyState is onAbort
	 */
	this.onAbort = function(parameters) {
		// Abort
	};

	/**
	 * event fired when request is async and readyState is onError
	 */
	this.onError = function(status, statusText, responseText, parameters) {
		// Error
	};

	/**
	 * constructor 
	 */
	hasXMLHttpRequest = typeof XMLHttpRequest != 'undefined' ? true : false;
	cache = new Cache();
	dummyThis = this; //hard to explain, but necessary

	this.setMethod(cMethod);
	this.setAsync(cAsync);
	this.setCachePolicy(typeof arguments[2] != 'undefined' ? cCachePolicy : CachePolicy.IGNORE);
}

var CachePolicy = {
	AVOID: 'avoid',
	USE_INTERNAL: 'internal',
	IGNORE: 'ignore'
}

function Cache() {
	var cachedRequests;

	/**
	 * gets a cached request
	 */
	this.get = function(pKey) {
		return cachedRequests[pKey];
	}

	/**
	 * puts a cached request
	 */
	this.put = function(pKey, pValue) {
		cachedRequests[pKey] = pValue;
	}

	/**
	 * returns true if the specified key has a request
	 */
	this.containsKey = function(pKey) {
		return (typeof cachedRequests[pKey] != 'undefined');
	}

	/**
	 * constructor
	 */
	cachedRequests = new Array();
}

function Parameters(data) {
	var inKeys = new Array();
	var inValues = new Array();

	/**
	 * remove all the given keys
	 */
	this.remove = function(key) {
		var newKeys = new Array();
		var newValues = new Array();

		for (var i = 0; i < inKeys.length; i++) {
			if (inKeys[i].toLowerCase() != key.toLowerCase()) {
				newKeys[newKeys.length] = inKeys[i];
				newValues[newValues.length] = inValues[i];
			}
		}

		inKeys = newKeys;
		inValues = newValues;
	}

	/**
	 * returns the keys
	 */
	this.keys = function() {
		return inKeys;
	}

	/**
	 * returns the values
	 */
	this.values = function() {
		return inValues;
	}

	/**
	 * add a new key-value pair
	 */
	this.put = function(key, value) {
		inKeys[inKeys.length] = key;
		inValues[inValues.length] = encodeURI(value);
	}

	/**
	 * get an array of values pair with the given key
	 */
	this.get = function(key) {
		var result = new Array();
		for (var i = 0; i < inKeys.length; i++) {
			if (inKeys[i].toLowerCase() == key.toLowerCase()) {
				result[result.length] = inValues[i];
			}
		}
		return result;
	}

	/**
	 * gets the size of the array of params
	 */
	this.size = function() {
		return inKeys.length;
	}

	/**
	 * tells if the array of params is empty
	 */
	this.isEmpty = function() {
		return (inKeys.length == 0);
	}

	/**
	 * string rapresentation of the array of params
	 */
	this.toString = function() {
		var dataString = '';

		for (var i = 0; i < inKeys.length; i++) {
			dataString = dataString + (dataString != '' ? '&' : '') + inKeys[i] + '=' + inValues[i];
		}

		return dataString;
	}

	/**
	 * return a number rapresenting how many key-value pairs has a given key
	 */
	this.howManyKeys = function(key) {
		var howMany = 0;
		for (var i = 0; i < inKeys.length; i++) {
			if (inKeys[i].toLowerCase() == key.toLowerCase()) {
				howMany++;
			}
		}
		return howMany;
	}

	/**
	 * return a number rapresenting how many key-value pairs has a given value
	 */
	this.howManyValues = function(value) {
		var howMany = 0;
		for (var i = 0; i < inValues.length; i++) {
			if (inValues[i].toLowerCase() == encodeURI(value).toLowerCase()) {
				howMany++;
			}
		}
		return howMany;
	}

	/**
	 * constructor 
	 */
	inKeys = new Array();
	inValues = new Array();
	if (data && typeof data == 'string') {
		var paramsNameValuePairs = data.split('&');
		for (var i = 0; i < paramsNameValuePairs.length; i++) {
			singleNameValuePair = paramsNameValuePairs[i].split('=');
			this.put(decodeURI(singleNameValuePair[0]), decodeURI(singleNameValuePair[1]));
		}
	}
}
