/* -------------------------------------------------------------------------- */
/** 
 *    @fileoverview
 *       XMLHttpRequest controller (Ajax engine).
 *
 *    @version rev013.2008-12-01
 *    @requires common.js
 */
/* -------------------------------------------------------------------------- */



/* --------------- XMLHttpRequest Emulation for old WinIE --------------- */

if (!window.XMLHttpRequest && ActiveXObject && !BA.ua.isMacIE) {
	window.XMLHttpRequest = function() {
		var msxml = ['Msxml2.XMLHTTP.5.0',
		             'Msxml2.XMLHTTP.4.0',
		             'Msxml2.XMLHTTP.3.0',
		             'Msxml2.XMLHTTP'    ,
		             'Microsoft.XMLHTTP' ];
		for (var i = 0, n = msxml.length; i < n; i++) {
			try {
				return new ActiveXObject(msxml[i]);
			} catch (err) { }
		}
		return null;
	};
}



/* --------------- Constructor : BAAjax inherits BAObservable --------------- */
/**
 * XMLHttpRequest controller (Ajax engine).
 * @class Ajax engine
 * @param {BAAjaxRequestArg} req    associative array { url, timeout, onComplete, onError, onAbort, aThisObject }
 * @constructor
 */
function BAAjax(req) {
	/** the embodiment XMLHttpRequest object
	    @type XMLHttpRequest @private */
	this.XMLHttpRequest = null;
	/** default timeout of the XMLHttpRequest (ms)
	    @type Number @private */
	this.timeout        = 120 * 1000; // 120 seconds.
	/** timer object to handle request timeout
	    @type BASetTimeout @private */
	this.timeoutTimer   = null;
	/** if request with useCache porpery, cache responseXML and reponseText
	    @type Object @private */
	this.cache          = {};
	/** if true, this instance is busy
	    @type Boolean @private */
	this.busy           = false;

	if (req) {
		this.request(req);
	}
}

BAAjax.prototype = new BAObservable;

/**
 * send request.
 * @param {BAAjaxRequestArg} req    associative array { url, timeout, onComplete, onError, onAbort, aThisObject } (required)
 */
BAAjax.prototype.request = function(req) {
	if (this.isBusy()) {
		throw 'BAAjax.request: this instance is currently busy.';
	} else if (typeof req != 'object' || !req) {
		throw 'BAAjax.request: request setting object is required.';
	} else if (typeof req.url != 'string') {
		throw 'BAAjax.request: request URL is not supplied.';
	} else {
		// create XMLHttpRequest instance.
		try {
			this.XMLHttpRequest = new XMLHttpRequest;
		} catch(err) {
			throw 'BAAjax.request: cannot create XMLHttpRequest instance.';
		}

		// request method decision
		req.method = (req.method || 'GET').toUpperCase();

		// set timeout timer.
		if (req.timeout >= 0) {
			this.timeout = req.timeout;
			if (this.timeout > 0) {
				this.timeoutTimer = new BASetTimeout(this.processRequestTimeout, this.timeout, this);
			}
		}

		// set callback functions.
		['onComplete', 'onError', 'onAbort'].forEach(function(eventName) {
			if (typeof req[eventName] == 'function') {
				this.addCallBack(eventName, req[eventName], req.aThisObject);
			}
		}, this);

		// query preparation.
		var param = '';
		if (typeof req.param == 'object') {
			var pairs = [];
			for (var name in req.param) {
				pairs.push([encodeURIComponent(name), encodeURIComponent(req.param[name])].join('='));
			}
			param = pairs.join('&');
			switch (req.method) {
				case 'GET'  : if (param) {
				              	req.url += (req.url.indexOf('?') != -1 ? '&' : '?') + param;
				              	param = '';
				              }
				              break;
				case 'POST' : break;
				default     : break;
			}
		}

		// XMLHttpRequest request send.
		this.XMLHttpRequest.onreadystatechange = BACreateDelegate(this.processStateChange, this);
		try {
			this.XMLHttpRequest.open(req.method, req.url, true);
			if (req.method == 'POST') {
				this.XMLHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
			}
			if (req.useCache) {
				this.XMLHttpRequest.url      = req.url;
				this.XMLHttpRequest.useCache = true;
				var Etag = '';
				var lastModified = '';
				if (this.cache[req.url]) {
					Etag         = this.cache[req.url].Etag;
					lastModified = this.cache[req.url].lastModified;
				}
				if (Etag) {
					this.XMLHttpRequest.setRequestHeader('If-None-Match', Etag);
				}
				if (lastModified) {
					this.XMLHttpRequest.setRequestHeader('If-Modified-Since', lastModified);
				}
			}
			this.XMLHttpRequest.send(param);
			this.busy = true;
		} catch(err) {
			this.doCallBack('onError', '403', 'Access to restricted URI denied');
			this.resetRequest();
		}
	}
}

/**
 * check XMLHTTPRequest state, process callbacks, and process next request in the buffer.
 * @private
 */
BAAjax.prototype.processStateChange = function() {
	var req = this.XMLHttpRequest;
	if (!this.aborted && req.readyState == 4) {
		var status = statusText = '';
		try {
			status     = req.status;
			statusText = req.statusText;
		} catch(err) {
			status     = '422';
			statusText = 'Unprocessable Entity';
		}

		if (status == '200' || (req.useCache && status == '304')) {
			var resXML  = req.responseXML;
			    resXML  = (resXML && resXML.documentElement) ? resXML : null;
			var resText = req.responseText;
			if (req.useCache) {
				var url = req.url;
				var cache = this.cache[url];
				if (status == '304' && cache) {
					resXML  = cache.responseXML  || null;
					resText = cache.responseText || '';
				} else {
					var lastModified = '';
					var Etag         = '';
					try {
						Etag         = req.getResponseHeader('Etag');
						lastModified = req.getResponseHeader('Last-Modified');
					} catch(e) {};
					this.cache[url] = {
						responseXML  : (resXML && resXML.documentElement) ? resXML : null,
						responseText : resText,
						lastModified : lastModified,
						Etag         : Etag
					}
				}
			}
			this.doCallBack('onComplete', resXML, resText   );
		} else {
			if (req.useCache) {
				this.cache[req.url] = null;
			}
			this.doCallBack('onError'   , status, statusText);
		}

		this.resetRequest();
	}
}

/**
 * process request timeout.
 * @private
 */
BAAjax.prototype.processRequestTimeout = function() {
	this.abort('408', 'Request Timeout');
}

/** 
 * abort current request arbitrarily.
 * @param {String} status         pseudo HTTP status code number (use '408' for timeout).
 * @param {String} statusText     abort message.
 */
BAAjax.prototype.abort = function(status, statusText) {
	if (this.XMLHttpRequest && this.isBusy()) {
		this.XMLHttpRequest.abort();
		status     = (typeof status     == 'string') ? status     : '';
		statusText = (typeof statusText == 'string') ? statusText : '';
		if (status == '408') {
			this.doCallBack('onTimeout', status, statusText);
		} else {
			this.doCallBack('onAbort'  , status, statusText);
		}
		this.resetRequest();
	}
}

/**
 * reset the request and clear timeout timer.
 * @private
 */
BAAjax.prototype.resetRequest = function() {
	if (this.timeoutTimer) {
		this.timeoutTimer.clearTimer();
		this.timeoutTimer = null;
	}
//	this.XMLHttpRequest = null;
	this.busy = false;
}

/**
 * get busy state of this instance.
 * @return true if this instance is busy.
 * @type Boolean
 */
BAAjax.prototype.isBusy = function() {
//	return Boolean(this.XMLHttpRequest);
	return this.busy;
}

/**
 * get responce header field value.
 * @param {String} name    header field name to get value
 * @return responce header field value.
 * @type String
 */
BAAjax.prototype.getResponseHeader = function(name) {
	if (typeof name != 'string' || !name) {
		throw 'BAAjax.getResponseHeader: first argument must be a string.';
	} else {
		try {
			return this.XMLHttpRequest.getResponseHeader(name);
		} catch(err) {
			return '';
		}
	}
}



/* --------------- Constructor : BAAjaxRequestArg --------------- */
/*! this is a description for JSDoc output... */
/**
 * request setting object for BAAjax.
 * @class BAAjax request setting
 * @constructor
 * @see BAAjax
 */
function BAAjaxRequestArg() {
	/** request method ('GET' or 'POST' or 'HEAD').
	    @type String */
	this.method      = '';
	/** url to request (this will be required when used in BAAjax.request()).
	    @type String */
	this.url         = '';
	/** request parameters (associative array of parameter name and its value).
	    @type Object */
	this.param       = {};
	/** milliseconds to request timeout.
	    @type Number */
	this.timeout     = 0
	/** function / method to be called when request completed.
	    @type Function */
	this.onComplete  = function(responseXML, responseText) { }
	/** function / method to be called when request error occurred.
	    @type Function */
	this.onError     = function(status, statusText) { }
	/** function / method to be called when request aborted Arbitrarily (except timeout).
	    @type Function */
	this.onAbort     = function(status, statusText) { }
	/** function / method to be called when request aborted by timeout.
	    @type Function */
	this.onTimeout   = function(status, statusText) { }
	/** this will be a global object ('this') in onComplete, onError, onAbort, onTimeout functions.
	    @type Object */
	this.aThisObject = null;
	/** if useCache is true, request with If-Modified-Since and/or ETag.
	    @type Boolean */
	this.useCache    = false;
}







/* -------------------- Main : register start-up -------------------- */

if (typeof BA == 'object' && BA.ua.isDOMReady && window.XMLHttpRequest) {
	if (!BA.env.isOnline) {
		throw 'BAAjax is unavailable on "${0}" scheme.'.formatTextBA([location.protocol]);
	} else {
		BAAddOnload(function() {
			BAAppendStateClassName('ajax-enabled');
		});
	}
}
