var AJAX_LOADING_START = 1;
var AJAX_LOADING_DONE  = 2;

/*******
	The 'options' parameter should be an object with any of the following values set:

	* error_func (function)
	    This function is called in the case of getting an error code (< 200 || >= 400) from the server.
	    If set, the normal callback function is NOT called.
	    If unset, the normal callback function WILL be called.

	* loading_func (function)
	    This function called when the request begins and ends.
	    The function should take a single parameter, which will be either AJAX_LOADING_START or AJAX_LOADING_DONE based on the state of the load.

	* param (any) 
	    If provided, this is passed as a parameter to the callback, timeout, and error functions.

	* synch (bool) 
	    If set to a true value, a synchronous request is made, instead of the default asynchronous request.

	* timeout (object)
	    Contains options for timeout settings. If not provided, no timeout will occur and the request will wait indefinitely.
	    The following values are recognized:
      * after (int)
          The number of milliseconds to wait before triggering a timeout. If this is 0, no timeout will occur.
      * callback (function)
          If an 'after' value (> 0) is provided, this function will be called if/when the timeout happens. If no callback function is provided,
	        a default function which simply aborts the request is used.
*******/

function AJAX(url, callback, options)
{
	if (!options)
		options = { };

	var timeout    = null;
	var xmlHttpReq = null;
	var param_str  = '';

	var m_initialized = false;

	var m_timeout_interval = 0;
	var m_on_timeout       = null;

	var m_on_success = callback;
	var m_on_failure = options.error_func;

	var m_num_retries = 0;

	try {
		xmlHttpReq = new XMLHttpRequest();
	}
	catch (ex) {
		try {
			xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
		}
		catch (ex) {
			throw "Your browser does not support AJAX, oddly";
		}
	}

	this.on_state_change = function()
	{
		if (options.loading_func && xmlHttpReq.readyState == 1) {
			options.loading_func(AJAX_LOADING_START, options.param);
		} else if (xmlHttpReq.readyState == 4) {
			clearTimeout(timeout);
			if (options.loading_func)
				options.loading_func(AJAX_LOADING_DONE);
			if (xmlHttpReq.status < 200 || xmlHttpReq.status >= 400) {
				if (options.error_func) {
					options.error_func(options.param);
					return;
				}
			}
			m_on_success(xmlHttpReq.responseXML, options.param);
		}
	}

	this.init = function()
	{
		// Initialize request properties
		xmlHttpReq.open('POST', url, (options.synch == 1) ? false : true);
		xmlHttpReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

		m_initialized = true;
	}

	var a = this;

	this.timeout = function()
	{
		// Whether or not we have a user callback function, we want to prevent the normal
		// handler from happening if the request returns while we're processing a timeout.
		xmlHttpReq.onreadystatechange = null;
		timeout = null;

		// If a callback function was provided, use it; otherwise, use default handler
		if (m_on_timeout) {
			var result = m_on_timeout(this, options.param);
			// If callback returns true, retry request
			if (result) {
				m_num_retries++;
				this.init();
				this.send();
			}
		} else {
			xmlHttpReq.abort();
		}
	}

	this.params = function(params)
	{
		// Reset internal parameter string
		param_str = '';

		// If 'params' is a string, use it directly
		if (typeof params == 'string') {
			param_str = params;
		} else if (typeof params == 'object') {
			// If 'params' is an object, build a string from its keys/values
			for (var p in params) {
				var data = encodeURIComponent(params[p]);
				param_str += (p + '=' + data + '&');
			}
		} else {
			throw "Can't process a(n) '" + (typeof params) + "' value";
		}
	}

	this.send = function(obj)
	{
		if (!m_initialized)
			this.init();

		if (typeof obj !== 'undefined')
			this.params(obj);

		// Add callback function
		if (callback)
			xmlHttpReq.onreadystatechange = this.on_state_change;

		// Add timeout handler
		if (typeof options.timeout === 'number') {
			// Legacy support for numeric 'timeout' and 'timeout_func' callback
			m_timeout_interval = options.timeout;
			m_on_timeout       = options.timeout_func;
		} else if (typeof options.timeout === 'object') {
			m_timeout_interval = options.timeout.after;
			m_on_timeout       = options.timeout.callback;
		}

		if (m_timeout_interval)
			timeout = setTimeout(function() { a.timeout(); }, m_timeout_interval);

		xmlHttpReq.send(param_str);
	}

	this.abort = function()
	{
		xmlHttpReq.onreadystatechange = null;
		xmlHttpReq.abort();

		if (timeout) {
			clearTimeout(timeout);
			timeout = null;
		}

		m_initialized = false;
	}

	this.get_callback = function()
	{
		return m_on_success;
	}

	this.set_callback = function(callback)
	{
		m_on_success = callback;
	}

	this.get_timeout = function()
	{
		return m_on_timeout;
	}

	this.set_timeout = function(callback)
	{
		m_on_timeout = callback;
	}

	this.retries = function()
	{
		return m_num_retries;
	}
}
