JSJaC_HAVEKEYS = false;  // whether to use keys
JSJaC_NKEYS    = 64;    // number of keys to generate

/* ******************************
 * JabberConnection 
 * somewhat abstract base class
 */

var JSJaCConnection = Class.create();

JSJaCConnection.prototype = {
	initialize: function() {
	},

	baseInitialize: function (oDbg, id) {
		if (oDbg && oDbg.log)
			this.oDbg = oDbg; // always initialise a debugger
		else {
			this.oDbg = new Object();
			this.oDbg.log = function() { };
		}
	
		this.id = id;
		this._connected = false;
		this._events = new Array();
		this._keys = null;
		this._ID = 0;
		this._pQueue = new Array();
		this._regIDs = new Object();
		this._req = new Array();   
	},

	showMsg: function(msg) {
		if (Rosters[this.id])
			Rosters[this.id].showMsg(msg);
	},
	
	connected: function() {
	   return this._connected;
	},
	
	getPollInterval: function() {
	   return this._timerval;
	},
	
	handleEvent: function(event,arg) {
		event = event.toLowerCase(); // don't be case-sensitive here
		this.oDbg.log("incoming event '"+event+"'",3);
		if (!this._events[event])
			return;
		this.oDbg.log("handling event '"+event+"'",2);
		//for (var i in this._events[event]) {
		for(i=0; i<this._events[event].length; i++){
			if (this._events[event][i]) {
				if (arg)
					this._events[event][i](arg);
				else
					this._events[event][i]();
			}
		}
	},
	
	registerHandler: function(event,handler) {
		event = event.toLowerCase(); // don't be case-sensitive here
		if (!this._events[event])
			this._events[event] = new Array(handler);
		else
			this._events[event] = this._events[event].concat(handler);
		this.oDbg.log("registered handler for event '"+event+"'",2);
	},
	
	setPollInterval: function(timerval) {
		if (!timerval || isNaN(timerval)) {
			this.oDbg.log("Invalid timerval: " + timerval,1);
			return -1;
		}
		this._timerval = timerval;
		return this._timerval;
	},

	_handlePID: function(aJSJaCPacket) {
	   var pID = aJSJaCPacket.getID();
		this.oDbg.log("Find handler for id: "+pID,3);
		if (!pID)
			return false;
		for (var i in this._regIDs) {
		//for (i=0; i < this._regIDs.length; i++) {
			if (this._regIDs[i] && i == pID) {
				this.oDbg.log("handling "+pID,3);
				this._regIDs[i].cb(aJSJaCPacket,this._regIDs[i].arg);
				this._unregisterPID(pID);
				return true;
			}
		}
		return false;
	},
	
	_process: function(timerval) {
		if (timerval)
			this.setPollInterval(timerval);
		this.send();
	},
	
	_registerPID: function(pID,cb,arg) {
		if (!pID || !cb)
			return false;
		this._regIDs[pID] = new Object();
		this._regIDs[pID].cb = cb;
		if (arg)
			this._regIDs[pID].arg = arg;
		this.oDbg.log("registered "+pID,3);
		return true;
	},
	
	_unregisterPID: function(pID) {
		if (!this._regIDs[pID])
			return false;
		delete this._regIDs[pID];
		this.oDbg.log("unregistered "+pID,3);
		return true;
	},

	_doReg: function() {
		/* ***
		 * In-Band Registration see JEP-0077
		 */
	
		var iq = new JSJaCIQ();
		iq.setType('set','reg1');
		var query = iq.setQuery('jabber:iq:register');
		query.appendChild(iq.getDoc().createElement('username')).appendChild(iq.getDoc().createTextNode(this.username));
		query.appendChild(iq.getDoc().createElement('password')).appendChild(iq.getDoc().createTextNode(this.pass));
			
		this.showMsg("Register");
		this.send(iq,this._doAuth.bind(this));
	},
	
	_doAuth: function(_iq) {
		/* ***
		 * Non-SASL Authentication as described in JEP-0078
		 */
	
		if (_iq && _iq.getType() == 'error') { // we failed to register
		   this.showMsg("Failed to Register");
			this.handleEvent('onerror',_iq.getNode().getElementsByTagName('error').item(0));
			return;
		}
	
		var iq = new JSJaCIQ();
		iq.setIQ(this.domain,null,'get','auth1');
		var query = iq.setQuery('jabber:iq:auth');
	
		var aNode = iq.getDoc().createElement('username');
		aNode.appendChild(iq.getDoc().createTextNode(this.username));
		query.appendChild(aNode);
	
	   this.showMsg("Send Username");
		this.send(iq,this._doAuth2.bind(this));
	},
	
	_doAuth2: function(_iq) {
		this.oDbg.log("got iq: " + _iq.xml(),4);
		var use_digest = false;
		for (var aChild=_iq.getNode().firstChild.firstChild; aChild!=null; aChild=aChild.nextSibling) {
			if (aChild.nodeName == 'digest') {
				use_digest = true;
				break;
			}
		}
	
		/* ***
		 * Send authentication
		 */
		var iq = new JSJaCIQ();
		iq.setIQ(this.domain,null,'set','auth2');
		var query = iq.setQuery('jabber:iq:auth');
		query.appendChild(iq.getDoc().createElement('username')).appendChild(iq.getDoc().createTextNode(this.username));
		query.appendChild(iq.getDoc().createElement('resource')).appendChild(iq.getDoc().createTextNode(this.resource));
	
		if (use_digest) { // digest login
			query.appendChild(iq.getDoc().createElement('digest')).appendChild(iq.getDoc().createTextNode(hex_sha1(this.streamid + this.pass)));
		} else { // use plaintext auth
			query.appendChild(iq.getDoc().createElement('password')).appendChild(iq.getDoc().createTextNode(this.pass));
		}
	
	   this.showMsg("Send Credentials");
		this.send(iq,this._doAuth3.bind(this));
	},
	
	/* ***
	 * check if auth' was successful
	 */
	_doAuth3: function(_iq) {
		if (_iq.getType() != 'result' || _iq.getType() == 'error') { // auth' failed
			this.disconnect();
			if (_iq.getType() == 'error')
				this.handleEvent('onerror',_iq.getNode().getElementsByTagName('error').item(0));
		} else
			this.handleEvent('onconnect');
	},
	
	/* ***
	 * send a jsjac packet. If we are using XMPP bind, just enqueue the request.
	 
	 * optional args: cb  - callback to be called when result is received)
	 *                arg - additional argument to be passed to callback
	 */
	send: function(aJSJaCPacket,cb,arg) {
		// remember id for response if callback present
		if (aJSJaCPacket && cb) {
			if (!aJSJaCPacket.getID())
				aJSJaCPacket.setID('JSJaCID_'+this._ID++); // generate an ID
	
			// register callback with id
			this._registerPID(aJSJaCPacket.getID(),cb,arg);
		}
	
		if (aJSJaCPacket) {
			try {
				this._pQueue = this._pQueue.concat(aJSJaCPacket.xml());
			} catch (e) {
				this.oDbg.log(e.toString(),1);
			}
		}
	
   		this._sendQueue();
		return;
	},
	
	_queueSuccess: function(transport) {
		this.oDbg.log("async recv: "+transport.responseText,4);
		this._handleResponse(transport);
		if (this.isPolling())
			this._timeout = setTimeout(this._sendQueue.bind(this), this.getPollInterval());
		else
			this._sendQueue();    // Always have at least one outstanding request.
	},
	
	_queueError: function(transport) {
		this.oDbg.log('XmlHttpRequest error',1);
		this._handleResponse(transport);
		if (this.isPolling())
			this._timeout = setTimeout(this._sendQueue.bind(this), this.getPollInterval());
		else
			this._sendQueue();    // Always have at least one outstanding request.
	},
	
	_queueException: function(request, e) {
		this.oDbg.log("XmlHttpRequest exception", 1, e);
//        if (e.message.indexOf('NS_ERROR_NOT_AVAILABLE') < 0)
//    		this._handleResponse(request.transport);
		if (this.isPolling())
			this._timeout = setTimeout(this._sendQueue.bind(this), this.getPollInterval());
		else
			this._sendQueue();    // Always have at least one outstanding request.
	},
	
	/*
	 * For XMPP bind:
	 * 1) If there's something to send, send it.
	 * 2) If there's nothing to send, but we don't have a request hangin out there, send an empty request.
	 */
	_sendQueue: function() {
		if (!this.connected()) {
			this.oDbg.log("Connection lost ...",1);
			return;
		}
	
		if (this._timeout)
			clearTimeout(this._timeout);
	
		var slot = this._getFreeSlot();
		
		if (slot < 0)
			return;
	
		if (typeof(this._req[slot]) != 'undefined' && this._req[slot].readyState != 4) {
			this.oDbg.log("Slot "+slot+" is not ready");
			return;
		}
		
		// If there's nothing queued but the next slot is busy, just return because we already have a poll request out.
		if (!this.isPolling() && this._pQueue.length == 0 && this._req[(slot+1)%2] && this._req[(slot+1)%2].readyState != 4) {
			return;
		}
	
		if (!this.isPolling())
			this.oDbg.log("Found working slot at "+slot,2);
	
		var successCB = this._queueSuccess.bind(this);
		var errorCB = this._queueError.bind(this);
		var exceptionCB = this._queueException.bind(this);
		
		var xml = '';
		while (this._pQueue.length) {
			var curNode = this._pQueue[0];
			xml += curNode;
			this._pQueue = this._pQueue.slice(1,this._pQueue.length);
		}
		var reqstr = this._getRequestString(xml);
		this.oDbg.log("sending: " + reqstr,4);
		
		this._req[slot] = this._setupRequest(true, reqstr,  {
		   onSuccess: successCB,
           on: errorCB,     // Possible in Firefox
		   onFailure: errorCB,
		   onException: exceptionCB
		});
	},
	
	syncSend: function(aPacket) {
		if (!aPacket)
			return;
	
		if (!this.connected()) {
			this.oDbg.log("Connection lost ...",1);
			return;
		}
	
		this.oDbg.log("sync send");
		
		/* can't do synchronuous send on http binding as it 
		 * would block until request timeouts 
		 */
		if (!this.isPolling()) {
			this.send(aPacket);
			return;
		}
		
		var reqstr = this._getRequestString(aPacket.xml());
		this.oDbg.log("sending: " + reqstr,4);
		var xmlhttp = this._setupRequest(false, reqstr);  
		this._handleResponse(xmlhttp);
	},
	
	/* ***
	 * send empty request 
	 * waiting for stream id to be able to proceed with authentication 
	 */
	_sendEmpty: function() {
		var reqstr = this._getRequestString();
		this.oDbg.log("sending: " + reqstr,4);
		var req = this._setupRequest(false, reqstr);
		this._getStreamID(); // handle response
	},
	
	_handleResponse: function(req) {
		var xmldoc = this._prepareResponse(req);
	
		if (!xmldoc)
			return null;
	
		this.oDbg.log("xmldoc.firstChild.childNodes.length: "+xmldoc.firstChild.childNodes.length,3);
		for (var i=0; i<xmldoc.firstChild.childNodes.length; i++) {
			this.oDbg.log("xmldoc.firstChild.childNodes.item("+i+").nodeName: "+xmldoc.firstChild.childNodes.item(i).nodeName,3);
			var aJSJaCPacket = JSJaCPWrapNode(xmldoc.firstChild.childNodes.item(i), this.oDbg);
			this.oDbg.log("1", 4);
			if (typeof(aJSJaCPacket.pType) == 'undefined') { // didn't parse as proper XMPP packet
				this.oDbg.log("2", 4);
				continue;
			}
			if (!this._handlePID(aJSJaCPacket))
				this.handleEvent(aJSJaCPacket.pType(),aJSJaCPacket);
		}
	
		return null;
	}
}

/* ***
 * an error packet for internal use
 */
function JSJaCError(code,type,condition) {
	var xmldoc = XmlDocument.create();
	if (xmldoc.documentElement)
		xmldoc.documentElement.appendChild(xmldoc.createElement('error'));
	else 
		xmldoc.appendChild(xmldoc.createElement('error'));

	xmldoc.firstChild.setAttribute('code',code);
	xmldoc.firstChild.setAttribute('type',type);
	xmldoc.firstChild.appendChild(xmldoc.createElement(condition));
	xmldoc.firstChild.firstChild.setAttribute('xmlns','urn:ietf:params:xml:ns:xmpp-stanzas');
	return xmldoc.firstChild.cloneNode(true);
}

/* ***
 * set of sha1 hash keys for securing sessions
 */											
function JSJaCKeys(func,oDbg) {
	var seed = Math.random();

	this._k = new Array();
	this._k[0] = seed.toString();
	this.oDbg = oDbg;

	for (var i=1; i<JSJaC_NKEYS; i++) {
		this._k[i] = func(this._k[i-1]);
		oDbg.log(i+": "+this._k[i],4);
	}

	this.indexAt = JSJaC_NKEYS-1;
	this.getKey = function() { 
		return this._k[this.indexAt--]; 
	};
	this.lastKey = function() { return (this.indexAt == 0); };
	this.size = function() { return this._k.length; };
}
