// Tx constructor

function Transaction() {

    if (typeof(skipjackTx) == "undefined" || skipjackTx == null) {
	skipjackTx = new Array();
    }
    
    this.txId = skipjackTx.length;
    skipjackTx[this.txId] = this;
    
    this.useAuth     = false;
    this.asyncMode   = false;
    this.waiting     = false;
    this.serverURL   = "";
    this.userName    = "";
    this.password    = "";
    this.paramNames  = new Array();
    this.paramValues = new Array();
    this.contentType = "transaction/map";
    this.unloading   = false;
    this.intervalID  = null;
    this.timeoutID   = null;

    this.setContentType = function (contentType) {
	this.contentType = contentType;
    }

    this.destroy = function () {
	if (this.intervalID != null) {
	    window.clearInterval(this.intervalID);
	    this.intervalID = null;
	}

	if (this.timeoutID != null) {
	    window.clearTimeout(this.timeoutID);
	    this.timeoutID = null;
	}

	this.cleanUp();
    }

    this.cleanUp = function () {
	if (this.waiting && this.xmlHttp && this.xmlHttp.abort) {
	    this.xmlHttp.abort();
	}

	if (this.xmlHttp && this.asyncMode) delete this.xmlHttp['onreadystatechange'];
	if (this.xmlHttp) delete this['xmlHttp'];
	if (this.intervalID == null && skipjackTx[this.txId]) delete skipjackTx[this.txId];
    }

    this.defaultErrorHandler = function (tx) {
	alert(tx.errorMsg);
    }
    
    this.errorHandler = this.defaultErrorHandler;
    
    this.setErrorHandler = function (errHandler) {
	if (typeof(errHandler) != "undefined" && errHandler != null)
	    this.errorHandler = errHandler;
	else
	    this.errorHandler = this.defaultErrorHandler;
    }

    this.clearErrorHandler = function () {
	this.errorHandler = this.defaultErrorHandler;
    }

    this.error = function(type, status, msg) {
	this.wasError    = true;
	this.errorType   = type;
	this.errorStatus = status;
	this.errorMsg    = msg;	
	this.errorHandler(this);
	
    }

    this.defaultStateChangeCallback = function (tx) {
	if (tx.xmlHttp.readyState==4) {
	    if (tx.xmlHttp.status == 200) {
		tx.setResponse();
		tx.waiting = false;
		if (tx.collectTimes) {
		    tx.endTx = new Date();
		}
		tx.completionCallback(tx);
		tx.cleanUp();
	    } else {
		tx.waiting     = false;
		tx.error("HTTP", tx.xmlHttp.status, tx.xmlHttp.statusText);
		tx.destroy();
	    }
	    return true;
	} else if (tx.xmlHttp.readyState==2) {
	    
	    // if status not available, it means the request failed
	    // (and therefore it will never get to state 3)
	    try {
		if (typeof(tx.xmlHttp.status) == "undefined") {
		    if (!tx.unloading) {
			tx.waiting = false;
			tx.error("HTTP", "", "Ready State 2 Error: " + tx.xmlHttp.statusText);
		    }
		    tx.destroy();
		    return true;
		}
	    } catch (e) {
		tx.waiting = false;
		tx.destroy();
		return true;
	    }
	}

	return false;
    }
    
    this.unload = function () {
	this.unloading = true;
	this.destroy();
    }

    this.stateChangeCallback = this.defaultStateChangeCallback;

    this.setStateChangeCallback = function (callback) {
	if (typeof(callback) != "undefined" && callback != null)
	    this.stateChangeCallback = callback;
	else
	    this.stateChangeCallback = this.defaultStateChangeCallback;
    }
    
    this.clearStateChangeCallback = function () {
	this.stateChangeCallback = this.defaultStateChangeCallback;
    }
    
    this.setAuthInfo = function (userName, password) {
	this.userName = userName;
	this.password = password;
	this.useAuth  = true;
    }

    this.clearAuthInfo = function () {
	this.userName = "";
	this.password = "";
	this.useAuth  = false;
    }

    this.setServerURL = function (baseURL) { 
	this.serverURL = baseURL;
    }

    this.setParam = function (paramName, paramValue) {
	if (paramName == "contentType") {
	    this.setContentType(paramValue);
	} else {
	    this.paramNames[this.paramNames.length] = paramName;
	    this.paramValues[this.paramValues.length] = escape(paramValue);
	}
    }
    
    this.clearParams = function () {
	this.paramNames  = new Array();
	this.paramValues = new Array();
    }

    this.setResponse = function () {
	this.responseStatus = this.xmlHttp.status;
	this.responseStatusText = this.xmlHttp.statusText;
	this.responseText   = this.xmlHttp.responseText;
    }


    this.getResultMap = function () {
	if (this.waiting) return null;

	var txMap = new TransactionMap(this.responseText);
	return txMap.map;
    }

	
    //
    // if completionCallback is non-null, the tx will be executed
    // asynchonously, and the callback will be executed when
    // it is done
    //
    // if stateChangeCallback is non-null, it will be called instead
    // of the default state change handler
    //

    this.execute = function (txName, completionCallback) {

	this.prepareTx(txName, completionCallback);

	return this.executeTx();

    }

    //
    // Prepare the Tx for execution. Internal method.
    //

    this.prepareTx = function(txName, completionCallback) {

	if (this.collectTimes) {
	    this.startPrep = new Date();
	}

	this.txName = txName;
	
	if (typeof(completionCallback) != "undefined" && completionCallback != null) {
	    this.completionCallback = completionCallback;
	    this.asyncMode = true;
	} else {
	    this.asyncMode = false;
	}
	
	var i;
	var params = new Array();
	for (i = 0;i < this.paramNames.length; i++) {
	    params[4 * i] = this.paramNames[i];
	    params[4 * i + 1] = "=";
	    params[4 * i + 2] = this.paramValues[i];
	    if (i < this.paramNames.length - 1) {
		params[4 * i + 3] = "&";
	    }
	}

	var url = this.txName;
	var idx = url.indexOf('?');


	if (idx > -1) {
	    this.paramStr = "contentType=" + this.contentType + '&' + url.substring(idx+1) + '&' + params.join("");
	    this.url = url.substring(0,idx);
	} else {
	    this.paramStr = "contentType=" + this.contentType + '&' + params.join("");
	    this.url = url;
	}

	return true;
    }

    //
    // Executes a prepared transaction. Internal method.
    //

    this.executeTx = function() {
	var txId = this.txId;

	this.xmlHttp = this.getHTTP();

	if (this.xmlHttp == null) {
	    this.error("UNSUPPORTED", "", "This browser does not support XmlHttpRequest processing.");
	    return false;
	}

	
	if (this.asyncMode) {
	    this.xmlHttp.onreadystatechange = function() {
		var id = txId;
		if (typeof(skipjackTx) != "undefined" &&
		    typeof(skipjackTx[id]) != "undefined" && 
		    skipjackTx[id] != null && 
		    skipjackTx[id].waiting) {
		    skipjackTx[id].stateChangeCallback(skipjackTx[id]);
		}
	    }
	}

	if (this.collectTimes) {
	    this.startTx = new Date();
	}

	if (this.useAuth) {
	    this.xmlHttp.open("POST", this.url, this.asyncMode, this.userName, this.password);
	} else {
	    this.xmlHttp.open("POST", this.url, this.asyncMode);
	}
	
	this.waiting = true;

	this.xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');	
	this.xmlHttp.send(this.paramStr);
	
	if (!this.asyncMode) {
	    this.setResponse();
	    delete this['xmlHttp'];
	    this.waiting = false;
	    if (this.collectTimes)
		this.endTx = new Date();
	    return this.responseStatus;
	}

    }

    //
    // Executes this transaction every 'millis' milliseconds.
    //

    this.executeInterval = function(txName, completionCallback, millis, immediate) {	
	this.prepareTx(txName, completionCallback);
	if (immediate) { this.executeTx(); }
	this.intervalID = window.setInterval("skipjackTx[" + this.txId + "].executeTx();", millis);
    }

    //
    // Executes this transaction after 'millis' milliseconds.
    //

    this.executeTimeout = function(txName, completionCallback, millis) {	
	this.prepareTx(txName, completionCallback);
	this.timeoutID = window.setTimeout("skipjackTx[" + this.txId + "].executeTx();", millis);
    }

    /**		
	Creates an HTTP request object for retreiving files.
	@return  HTTP request object.
    */    	
    this.getHTTP = function() {
	var obj;
	try{ //to get the mozilla httprequest object
	    obj = new XMLHttpRequest();
	}catch(e){
	    try{ //to get MS HTTP request object
		obj=new ActiveXObject("Msxml2.XMLHTTP.4.0");
	    }catch(e){
		try{ //to get MS HTTP request object
		    obj=new ActiveXObject("Msxml2.XMLHTTP")
		}catch(e){
		    try{// to get the old MS HTTP request object
			obj = new ActiveXObject("microsoft.XMLHTTP"); 
		    }catch(e){
			try{//to create the ASV request object.
			    obj = new ASVRequest();
			}catch(e){
			    return null;
			}
		    }
		}    
	    }
	}
	return obj;
    }
    
}

function TransactionMap(mapText) {
    this.curIdx = 0;
    this.mapText = mapText;
    this.map = new Object();

    this.parseDone = function () {
	return this.curIdx >= mapText.length;
    }

    this.nextToken = function (buffer) {

	if (typeof(buffer) == "undefined" || buffer == null) {
	    buffer = new Array();
	}
	
	buffer.length = 0;

	var done = this.parseDone();
	
	while (!done) {
	    c = mapText.charAt(this.curIdx++);
	    
	    if (c == '|') {
		done = true;
		this.lastTokenEOL = false;
	    } else if (c == '\n') {
		done = true;
		this.lastTokenEOL = true;
	    } else {
		if (c == '+')
		    buffer[buffer.length] = ' ';
		else
		    buffer[buffer.length] = c;
	    }
	}
	
	return unescape(buffer.join(""));
    }

    this.parse = function () {
	var type, key;
	var abort = false;
	var buffer = new Array();
	while (!abort && !this.parseDone()) {
	    type = this.nextToken(buffer);
	    if (type == "K") {
		key = this.nextToken(buffer);

		// eat garbage (should never execute....)
		while (!this.lastTokenEOL)
		    this.nextToken(buffer);
		
		type = this.nextToken(buffer);
		if (type == "V") {
		    this.map[key] = this.nextToken(buffer);
		} else if (type == "L") {
		    var arr = new Array();
		    var len = this.nextToken(buffer);
		    var i;
		    for (i = 0; i < len; i++) {
			type = this.nextToken(buffer);
			if (type == "V") {
			    arr[i] = this.nextToken(buffer);
			} else {
			    this.map["wasError"]     = true;
			    this.map["errorToken"]   = type;
			    this.map["expectedType"] = "vector";
			    this.map["errorMsg"]     = "Error parsing transaction map. Expected vector element and got type " + type;
			    abort = true;
			}
			this.map[key] = arr;
		    }
		} else if (type == "T") {
		    var done = false;
		    while (!done && !this.parseDone() && !this.lastTokenEOL) {
			this.nextToken(buffer);
			if (this.lastTokenEOL) {
			    type = this.nextToken(buffer);			    
			    if (type == "K") {
				// if we found a key, we're done eating the table, just backup 
				done = true;
				this.curIdx -= 2;
			    }
			}
		    }
		    this.map[key] = "JavaScript parsing of Table elements has not been implemented.";

		} else {
		    this.map["wasError"]     = true;
		    this.map["errorToken"]   = type;
		    this.map["expectedType"] = "vector";
		    this.map["errorMsg"]     = "Error parsing transaction map. Unknown type " + type;
		    abort = true;
		}
		
	    } else {
		this.map["wasError"]     = true;
		this.map["errorToken"]   = type;
		this.map["expectedType"] = "key";
		this.map["errorMsg"]     = "Error parsing transaction map. Expected key type and got type " + type;
		abort = true;
	    }
	}
	
	this.map["wasError"] = abort;
    }   


    this.parse();
}

function skipjackTxUnload(e) {
    if (window.addEventListener)
	window.removeEventListener('unload', skipjackTxUnload, false);
    else {
	window.detachEvent('onunload', skipjackTxUnload);
    }
    
    if (typeof(skipjackTx) == "undefined") return;
    
    var id;
    for (id in skipjackTx) {
	skipjackTx[id].unload();
    }
}

// register the unload handler

if (window.addEventListener)
    window.addEventListener('unload', skipjackTxUnload, false);
else {
    window.detachEvent('onunload', skipjackTxUnload);
    window.attachEvent('onunload', skipjackTxUnload);
}
