/**
 * @fileoverview
 * 
 * The ajax.lib.js JavaScript library provides objects to extend in order to
 * implement event-driven applications, as well as a full XMLHttpRequest
 * wrapper object, with an accompanying class to manage garbage collection
 * and pooling.
 * 
 <pre>
 	Copyright (c) 2006 Frozen O Productions
	Written by Shawn Lauriat
	All rights reserved.
 
	Redistribution and use in source and binary forms, with or without
	modification, are permitted provided that the following conditions are met:

	- Redistributions of source code must retain the above copyright notice,
		this list of conditions and the following disclaimer.
	- Redistributions in binary form must reproduce the above copyright notice,
		this list of conditions and the following disclaimer in the
		documentation and/or other materials provided with the distribution.
	- Neither the name of Frozen O Productions nor the names of its
		contributors may be used to endorse or promote products derived from
		this software without specific prior written permission.
 
	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
	AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
	IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
	ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
	LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
	CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
	POSSIBILITY OF SUCH DAMAGE.
 </pre>
 */

/**
 * The base class for all custom events, rather than attempting to extend
 * the native JavaScript Exception object, which wouldn't work anyway.
 * @construct
 * @base
 */
function CustomEvent() { }
CustomEvent.prototype = {
	type : 'custom'
}

/**
 * Custom EventTarget equivalent
 * @construct
 * @base
 */
function EventDispatcher() { }
EventDispatcher.prototype = {
	/**
	 * An object literal to store arrays of listeners by type
	 */
	events : {},
	
	/**
	 * If it supports the type, add the listener (capture ignored)
	 * @param {String} type The type of event to add the listener to ("load", etc.)
	 * @param {Object} listener Either a function reference or an array containing
	 * references to the function and the object within whose context the function
	 * needs to run.
	 * @param {boolean} capture Unused, just there to emulate real events.
	 */
	addEventListener : function(type, listener, capture) {
		if (this.events[type]) {
			this.events[type].push(listener);
		}
	},
	
	/**
	 * If it supports the type, remove the listener (capture ignored)
	 * @param {String} type The type of event to add the listener to ("load", etc.)
	 * @param {Object} listener Either a function reference or an array containing
	 * references to the function and the object within whose context the function
	 * needs to run.
	 * @param {boolean} capture Unused, just there to emulate real events.
	 */
	removeEventListener : function(type, listener, capture) {
		if (this.events[type] == undefined) {
			return;
		}
		var index = this.events[type].indexOf(listener);
		if (this.events[type][index]) {
			this.events[type].splice(index, 1);
		}
	},
	
	/**
	 * Cycle through all of the event listeners, passing the event to the callbacks,
	 * generally only called internally by the class extending EventDispatcher
	 * @param {String} type The type of event to add the listener to ("load", etc.)
	 * @param {CustomEvent} event The CustomEvent (or subclass of) to pass to each
	 * listener for the given event type.
	 * @see CustomEvent
	 */
	dispatchEvent : function(type, event) {
		if (this.events[type]) {
			for (var i in this.events[type]) {
				if (typeof this.events[type][i] == 'function') {
					this.events[type][i](event);
					// Accepts an array of the contextual object and the function to call
				} else if (typeof this.events[type][i] == 'object') {
					this.events[type][i][1].call(this.events[type][i][0], event);
				}
			}
		}
	}
}

// A CustomEvent to pass AjaxRequests when loaded
function AjaxEvent(request) {
	this.request = request;
}
AjaxEvent.prototype = new CustomEvent;
AjaxEvent.prototype.type = 'ajax';
AjaxEvent.prototype.request = null;

// Instantiated by the AjaxRequestManager, not directly
function AjaxRequest(id) {
	this.id = id;
	
	// If the browser follows the standard
	if (window.XMLHttpRequest) {
		this.xhr = new XMLHttpRequest();
		// ...otherwise, if Internet Explorer < 7
	} else if (window.ActiveXObject) {
		this.xhr = new ActiveXObject('Microsoft.XMLHTTP');
	}
	// Callback for this.xhr.onreadystatechanged
	this.xhr.onreadystatechange = new Function("AjaxRequest.prototype.stateChanged.apply(requests["+id+"], arguments);");
}
AjaxRequest.prototype = new EventDispatcher;
// Event dispatching
AjaxRequest.prototype.events = {
		abort : [],
		data : [],
		internalservererror : [],
		load : [],
		notfound : [],
		notmodified : [],
		open : [],
		partialload : [],
		requestedrangenotsatisfiable : [],
		send : [],
		unauthorized : []
	};
AjaxRequest.prototype.statusCodeEvents = {
		200 : 'load',
		206 : 'partialload',
		304 : 'notmodified',
		401 : 'unauthorized',
		404 : 'notfound',
		416 : 'requestedrangenotsatisfiable',
		500 : 'internalservererror'
	};
// Used to emulate this meaning this
AjaxRequest.prototype.id = null;
AjaxRequest.prototype.xhr = null;
AjaxRequest.prototype.aborted = false;

// Store key/value pairs for the request headers
AjaxRequest.prototype.headers = {};

// Store variable/value pairs for the GET request
AjaxRequest.prototype.get = {};

// Store variable/value pairs for the POST request
AjaxRequest.prototype.post = {};

// Decide whether or not to send this.post
AjaxRequest.prototype.method = 'POST';

// Callback for this.xhr.onreadystatechanged
AjaxRequest.prototype.stateChanged = function() {
	// Only trigger load if finished returning
	switch(this.xhr.readyState) {
		case 3:
			var e = new AjaxEvent(this);
			this.dispatchEvent('data', e);
			break;
		case 4:
			try {
				if (this.statusCodeEvents[this.xhr.status]) {
					var e = new AjaxEvent(this);
					this.dispatchEvent(this.statusCodeEvents[this.xhr.status], e);
				}
			} catch(e) { }
	}
}

// Simple alias to abort the call
AjaxRequest.prototype.abort = function() {
	this.aborted = true;
	var event = new AjaxEvent(this);
	event.returned = this.xhr.abort();
	this.dispatchEvent('abort', event);
	return event.returned;
}

// Send all of the headers specified
AjaxRequest.prototype.sendHeaders = function() {
	for (i in this.headers) {
		this.xhr.setRequestHeader(i, this.headers[i]);
	}
}

// Alias to this.xhr.open, which stores the method in
// order to decide whether to bother concatinating
// this.post into url-encoded string form. Note: this
// only takes the baseurl as its url, since it encodes
// and concatinates this.get into the GET parameters.
AjaxRequest.prototype.open = function(method, url) {
	this.method = method.toUpperCase();
	var real_get = this.urlEncodeObject(this.get);
	url += '?' + real_get;
	var async = (typeof arguments[2] != "boolean") ? true : arguments[2];
	var user = (typeof arguments[2] != "String") ? null : arguments[3];
	var pass = (typeof arguments[2] != "String") ? null : arguments[4];
	var event = new AjaxEvent(this);
	event.returned = this.xhr.open(
					this.method,
					url,
					async,
					user,
					pass
				);
	this.dispatchEvent('open', event);
	return event.returned;
}

// Simple alias to this.xhr.send, adjusting this.post
// depending on the request method specified.
AjaxRequest.prototype.send = function() {
	if (this.aborted) {
		return false;
	}
	this.sendHeaders();
	var real_post = '';
	var event = new AjaxEvent(this);
	if (this.method == 'POST') {
		this.xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		real_post = this.urlEncodeObject(this.post);
		event.returned = this.xhr.send(real_post);
	} else {
		event.returned = this.xhr.send("");
	}
	this.dispatchEvent('send', event);
	return event.returned;
}

// Non-recursive serialization from object to
// url-encoded values
AjaxRequest.prototype.urlEncodeObject = function(obj) {
	var first = true;
	var string = '';
	var temp_key;
	var temp_obj;
	for (i in obj) {
		temp_key = encodeURIComponent(i);
		switch (typeof obj[i]) {
			case 'number':
				temp_obj = obj[i];
				break;
			case 'boolean':
				temp_obj = (obj[i]) ? 1 : 0;
				break;
			case 'undefined':
				temp_obj = '';
				break;
			default:
				temp_obj = encodeURIComponent(obj[i]);
				break;
		}
		if (first) {
			first = false;
			string += temp_key + '=' + temp_obj;
		} else {
			string += '&' + temp_key + '=' + temp_obj;
		}
	}
	return string;
}

// Manage pool of AjaxRequest instances
function AjaxRequestManager() { }
AjaxRequestManager.prototype = {
	// Array of AjaxRequest instances
	requests : [],
	// Event listeners to auto-add to new requests
	events : AjaxRequest.prototype.events,
	
	// Factory-type function to instanciate AjaxRequests
	createAjaxRequest : function() {
		var new_id = ++requests.length;
		try {
			requests[new_id] = new AjaxRequest(new_id);
			requests[new_id].events = this.events;
			return requests[new_id];
		} catch (e) {
			alert(e);
			// Clean up junk reference if necessary
			if (requests[new_id]) {
				requests.pop();
			}
			return false;
		}
	},

	// Garbage collection
	eliminateAjaxRequest : function(req) {
		if (!req || !req.id || !requests[req.id]) {
			return false;
		}
		var id = req.id;
		// Call abort in case of current activity
		requests[id].abort();
		// First, delete the reference
		requests.splice(id, 1);
		// Then, adjust the references of the remaining
		// objects to match their new indices
		while (id < requests.length) {
			requests[id++].id--;
		}
		return true;
	},

	// Provide a method to cancel all active and pending requests
	abortAll : function() {
		for (var i = 0; i < window.requests.length; i++) {
			if (window.requests[i]) {
				window.requests[i].abort();
			}
		}
	},
	
	// Auto-add listeners to AjaxRequest events
	addEventListener : function(type, listener, capture) {
		EventDispatcher.prototype.addEventListener.call(this, type, listener);
	},
	
	// If it supports the type, remove the listener (capture ignored)
	removeEventListener : function(type, listener, capture) {
		EventDispatcher.prototype.removeEventListener.call(this, type, listener);
	}
}

// Global pool of AjaxRequest objects
var requests = [];
// Global Singleton of the AjaxRequestManager
var request_manager = new AjaxRequestManager();
