/* $Id: request_broker.js,v 1.8 2010-07-16 12:13:58 jcheese Exp $
 * Copyright (c) 2010 Orbis Technology Ltd. All rights reserved.
 *
 * The Request Broker provides prioritised asynchronous AJAX queues. Each priority is
 * assigned a queue (associated array indexed by the priority) that serialises the requests. The
 * requests are still asynchronous, but ordered!
 *
 * The broker will handle any errors with the request before calling the user supplied 'callback'.
 *
 * Use the broker in environments which you cannot control the ordering of the AJAX requests.
 *
 * Original source, openbet office shared javascript: request_broker.js
 * Id: request_broker.js,v 1.2 2010-04-15 14:46:50 cnagy Exp
 */

if(window.cvsID) {
	cvsID('request_broker', '$Id: request_broker.js,v 1.8 2010-07-16 12:13:58 jcheese Exp $', 'office');
}



/*
 * To avoid multiple brokers being instantiated, this class is wrapped in a
 * simple Singleton Pattern implementation.
 */
SA_singleton_request_broker = function() {
	throw "Cannot be instantiated";
}

/*
 * Instantiates RequestBroker for use - ensures that only one instantiation occurs.
 */
SA_singleton_request_broker.instance = function () {
	if (this._instance) {
		return this._instance;
	}

	
/**********************************************************************
 * BEGIN PRIVATE OBJECT DEFINITION
 *********************************************************************/



/**********************************************************************
 * RequestBroker
 *********************************************************************/

var RequestBroker = function (_opt)
{
	this._init(_opt);
}



/* Constructor
 *
 *   _opt - optional arguments (associated array)
 *          'url'           - server url; default null (must be supplied per send request)
 *          'indicator_cb'  - indicator/animated-GIF callback function; default null
 *                            signature:
 *                               function name(_enable) { ... }
 *                            where
 *                               _enable - if true then should start animated GIF, else stop
 *          'max_priority'  - maximum priority; default 10
 *                            keep this maximum small, as can impact performance
 */
RequestBroker.prototype._init = function(_opt)
{
	var def = {
		'url'         : null,
		'indicator_cb': null,
		'max_priority': 10
	};

	_opt = associatedArray(def, _opt);

	// check arguments
	if(_opt.url !== null && typeof _opt.url !== 'string') {
		RequestBroker._error(['invalid url \'', _opt.url, '\''].join(''));
	}
	if(_opt.indicator_cb !== null && typeof _opt.indicator_cb !== 'function') {
		RequestBroker._error('invalid indicator callback');
	}
	if(typeof _opt.max_priority !== 'number' || _opt.max_priority < 0) {
		RequestBroker._error(['invalid max priority \'', _opt.max_priority, '\''].join(''));
	}


	this.queue        = {};
	this.busy         = false;
	this.req_num      = 1;
	this.url          = _opt.url
	this.indicator_cb = _opt.indicator_cb;
	this.max_priority = _opt.max_priority;
};



/* Send a request.
 *
 * The request might not be sent immediately if busy, but will be queued. Duplicate request will
 * ignored.
 *
 *          _opt - optional arguments (associated array)
 *                 'priority' - request pririty; default 0
 *                              the request is added to the queue indexed by the priority, lowest priority
 *                              takes precedence
 *          'url'             : server url; default this.url
 *                              if not supplied will use url set on construction
 *          'method'          : HTTP method (GET|POST); default 'POST'
 *          'action'          : POST action argument; default null
 *                              only applicable if POSTing an associated array
 *          'post'            : post/get arguments; default null
 *                              can be supplied as a string, e.g. '&name=value&name1=value1...' or as an
 *                              associated array (name value pairs)
 *                              if supplying an associated array, then action must be supplied
 *          'async'           : Asynchronous request; default true
 *                              if non-async, the request is not queued and is sent immeditatly, regadless
 *                              of priority and queue size
 *          'callback'        : request callback; default null
 *                              called on successully reciving an AJAX response
 *                              signature:
 *                                   function name(_response) { ... }
 *                              where:
 *                                   _response    AJAX response object
 *          'debug'           : debug mode; default false
 *                              logs messages to firebug if enabled (printfire must be enabled)
 *          'nocache'         : Generates a random number on the url of the request in order to prevent caching
 *          'nocache_harsh'   : Where the req_num is the same because the URL is unchanged but parameters in it
 *                              now have alternative context due other actions we append a datestring to the
 *                              URL.
 *          'timeoutmsecs'    : if supplied it will invalidate the request after N msecs. This means that
 *                              should a response arrive after that time it will be ignored. IF a 
 *                              timeoutcallback is supplied it will be executed at that point.
 *          'timeoutcallback' : If supplied along with timeoutmsecs this callback will be executed upon timeout
 */
RequestBroker.prototype.send = function(_opt)
{

	var def = {
		'priority'        : 0,
		'url'             : this.url,
		'method'          : 'POST',
		'action'          : null,
		'post'            : null,
		'async'           : true,
		'callback'        : null,
		'debug'           : false,
		'nocache'         : false,
		'nocache_harsh'   : false,
		'timeoutmsecs'    : null,
		'timeoutcallback' : null
	},
	c, a;

	_opt = associatedArray(def, _opt);

	// check arguments
	var methods = ['url', 'method', 'callback'];
	
	for(var curmethod = 0; curmethod < methods.length; curmethod++) {
		if(_opt[(c = methods[curmethod])] === null || !_opt[c].length) {
			RequestBroker._error(['invalid ', c].join(''));
		}
	}
	if(typeof _opt.callback !== 'function') {
		RequestBroker._error('invalid callback');
	}
	if(!/^POST|GET$/.test((_opt.method = _opt.method.toUpperCase()))) {
		RequestBroker._error(['invalid method \'', _opt.method, '\''].join(''));
	}
	if(_opt.method === 'POST') {
		if(_opt.post === null) {
			RequestBroker._error(['post argument is not supplied for POST method']);
		}
		if(typeof _opt.post === 'object' && (_opt.action === null || !_opt.action.length)) {
			RequestBroker._error(['action argument is not supplied for POST object']);
		}
	}
	if(
		typeof _opt.priority !== 'number' ||
		_opt.priority < 0 ||
		_opt.priority > this.max_priority
	) {
		RequestBroker._error(['invalid priority \'', _opt.priority, '\''].join(''));
	}
	if(typeof _opt.async !== 'boolean') {
		RequestBroker._error(['invalid asyncronous flag \'', _opt.async, '\''].join(''));
	}
	if(typeof _opt.debug !== 'boolean') {
		RequestBroker._error(['invalid debug flag \'', _opt.debug, '\''].join(''));
	}
	if(_opt.debug && !window.printfire && typeof window.printfire === 'function') {
		_opt.debug = false;
	}


	// convert post arguments if supplied as an object
	if(_opt.method === 'POST' && typeof _opt.post === 'object') {
		_opt.post = RequestBroker.build_post(_opt.action, _opt.post);
	}


	// non-async request sent immediately
	if(!_opt.async) {
		this._send(null, _opt);
		return;
	}


	// add arguments to queue, indexed by priority
	var queue = this.queue,
	i = 0,
	p = _opt.priority,
	len;

	if(typeof queue[p] === 'undefined') queue[p] = [];

	// -check if the request is already within the queue
	_opt.checksum = [_opt.url, _opt.method, _opt.post].join('|');
	for(a = queue[p], len = a.length; i < len; i++) {
		if(a[i].checksum === _opt.checksum) {
			if(_opt.debug) {
				printfire('RequestBroker.send: ignorning duplicate request ', _opt.checksum);
			}
			return;
		}
	}

	if (_opt.nocache || _opt.nocache_harsh) {
		var msg = [];
		msg[msg.length] = _opt.url;
		if (_opt.url.search("\\?") >= 0) {
			msg[msg.length] = '&';
		} else {
			msg[msg.length] = '?';
		}
		msg[msg.length] = 'no_cache=';
		if (_opt.nocache) {
			msg[msg.length] = this.req_num;
		} else {
			 msg[msg.length] = new Date().getTime();
		}

		_opt.url = msg.join('');
	}

	var self = this;

	if ( (_opt.timeoutmsecs) && (_opt.timeoutmsecs > 0) ) {
		_opt.timeoutId = setTimeout( function(){ self._to(_opt) }, _opt.timeoutmsecs);
	}

	// -push request to queue
	a[a.length] = _opt;


	// send
	this._send(_opt.debug);
};

/* Private function to handle TimeOuts
 *
 *
*/
RequestBroker.prototype._to = function(_req)
{
	// invalidate both the timeoutId and the callback
	_req.timeoutId = false;
	_req.callback  = false;
	// execute the timeoutcallback
	_req.timeoutcallback();
}

/* Private function to send a request.
 *
 * If trying to send an asynchrouns request, looks on the all the priority queues (starting at 0)
 * for the next request and sends to server. If busy, will immediately return and wait for the
 * current request to finish and send.
 * If the queues are empty, exits
 *
 * Will send a synchrouns request immediately.
 *
 *   _debug - debug mode (logs to firebug); default none
 *   _req   - synchrounous request; default none;
 *            if supplied, then will send synchrounously, else looks on the queue
 *
 */
RequestBroker.prototype._send = function(_debug, _req)
{
	// find next request
	if(typeof _req === 'undefined') {

		// busy
		if(this.busy) {
			if(typeof _debug === 'boolean' && _debug) {
				printfire('RequestBroker._send: busy');
			}
			return;
		}

		var queue = this.queue,
		priority = 0,
		p;


		// find the lowest priority number
		for(; priority <= 100; priority++) {
			for(p in queue) {
				if(p <= priority && queue[p].length) {
					priority = p;
					break;
				}
			}
			if(priority === p) break;
		}

		// queue empty
		if(typeof queue[priority] === 'undefined' || !queue[priority].length) {
			return;
		}


		// get 1st item (oldest) in the queue
		_req = queue[priority].shift();
	}

	var self = this;


	// send request
	this._indicator(true);
	if(_req.debug) {
		printfire('RequestBroker._send: ',
				  (_req.num = this.req_num++),
				  _req.async,
				  _req.url,
				  _req.method,
				  _req.post);
	}

	// -denote busy if sending an asynchronous request
	//  only sending 1 async at a time
	if(_req.async) this.busy = true;

	HttpRequest(_req.url,
				_req.method,
				function(_r) { self._cb(_req, _r); },
				_req.post,
				_req.async);
};



/* Private function to handle an AJAX response callback
 * Will display an error if the status is not 200, else calls the user-supplied callback.
 * When complete, and handling an asynchrounous request, re-calls _send to process next request.
 *
 * Callback is protected by a try-catch block, exceptions will be logged on firebug or an alert
 *
 *   _req   - request
 *   _resp  - AJAX response
 */
RequestBroker.prototype._cb = function(_req, _resp)
{
	if(_req.debug) {
		printfire('RequestBroker._cb: ', _req.priority, _req.num, _resp.status, _resp.statusText);
	}


	// bad response
	if(_resp.status !== 200) {
		var m = ['Failed to load request '];
		if(_req.debug) {
			m[m.length] = '(',
			m[m.length] = _req.num,
			m[m.length] = ') ';
		}
		m[m.length] = _resp.status;
		m[m.length] = ' ';
		m[m.length] = _resp.statusText;
		m = m.join('');

		if(window.errorfire) errorfire('RequestBroker._cb: ', m);
		if(window.PopupAlert) PopupAlert(document.title, m);
	}

	// request callback
	else {
		try {
			if (_req.timeoutId) clearTimeout(_req.timeoutId);
			
			if (_req.callback)  _req.callback(_resp);
		}
		catch(_e) {
			var m = ['Failed to call callback '];
			if(_req.debug) {
				m[m.length] = '(',
				m[m.length] = _req.num,
				m[m.length] = ') ';
			}
			m[m.length] = _e.message;
			m = m.join('');
			if(window.errorfire) errorfire('RequestBroker._cb: ', m);
			else alert(m);
		}
	}


	// next request
	if(_req.async) {
		this.busy = false;
		this._send();
	}


	this._indicator(false);
};



/* Private function to throw an exception
 *
 *   _msg - message
 */
RequestBroker._error = function(_msg)
{
	throw ['RequestBroker: ', _msg].join('');
};



/* Private function to handle an indicator
 * Will enable indicator on first request, and stop on the last
 *
 *   _enable  - enable the indicator
 */
RequestBroker.prototype._indicator = function(_enable)
{
	if(this.indicator_cb === null) return;

	if(_enable) {
		if(typeof this.indicator_count === 'undefined') this.indicator_count = 1;
		else this.indicator_count++;

		if(this.indicator_count === 1) this.indicator_cb(true);
	}
	else if(--this.indicator_count === 0) {
		this.indicator_cb(false);
	}
};



/* Build a post request
 *
 *   _action  - post action
 *   _nvp     - name value list
 *   returns  - post request (string of name value pairs delimited by =)
 */
RequestBroker.build_post = function(_action, _nvp)
{
	var post = [['action=', _action].join('')],
	name, i, len, a;

	for(name in _nvp) {
		if(typeof _nvp[name] == 'object') {
			for(i = 0, a = _nvp[name], len = a.length; i < len; i++) {
				post[post.length] = ['&', name, '=', a[i]].join('');
			}
		}
		else {
			post[post.length] = ['&', name, '=', _nvp[name]].join('');
		}
	}

	return post.join('');
};

	
/**********************************************************************
 * END PRIVATE OBJECT DEFINITION
 *********************************************************************/

	this._instance = new RequestBroker(	{
		'max_priority': 2
	} );
	return this._instance;
}

