
//
// AjaxEngine.js
// Justin Alpino
// http://www.jalpino.com
// November, 2005
//
// AjaxEngine is a client-side object that is used to facilitate discrete requests to the server and answer the responses.
// This Object was largely inspired by Getahead's DWREngine (which can found at http://www.getahead.ltd.uk/dwr/). 
// AjaxEngine borrows some features like the post and pre hook but also includes an entirely new set of functionality and 
// control over your 'ajax' calls.
//
// This engine is free to use, just leave my original heading above. Thanks,
// 

// XMLhttpRequest Constants
http_UNINITIALIZED = 0;
http_LOADING = 1;
http_LOADED = 2;
http_INTERACTIVE = 3;
http_COMPLETE = 4;
		
// Construct the Ajax Engine
function AjaxEngine(){
}

// Initialize the Helper Object
AjaxEngine.Helper = function(){

}

// Public Members
AjaxEngine.gatewayURI = "http://"+ document.location.host +"/new/index.cfm";
AjaxEngine.callMap = [];
AjaxEngine.setPreHook = null;
AjaxEngine.setPostHook = null;
AjaxEngine.debugMode = false;
AjaxEngine._preHook = null;
AjaxEngine._postHook = null;

// Public Methods
// Sends a request to the server with key=value pairs or nothing at all
AjaxEngine.send = function( callback, event ){
	
	// verify that the callback is a function and that the method has length
	if(typeof callback != 'function')
		throw "AjaxEngine: the callback '"+ callback.toString() +"' is not a valid function or is broken.";
	
	// store the parameters data if any is passed in
	var data = arguments[2] ? arguments[2] : null;
		
	// create a unique ID for the request
	var rid = this.Helper.getUUID().toString();

	// create a new request object in the callmap
	this.callMap[rid] = new Request( rid, this.gatewayURI, event, data, "GET" );
	
	// Bind the callback function
	this.callMap[rid].setStatusCodeListener(200, callback);

	//Set the ready state 
	this.callMap[rid].setOnReadyStateHandler( function (){ AjaxEngine.listen( rid );} );
	
	// Execute the preHook
	if( this._preHook != null)
		this._preHook();
	
	// Send the request
	this.callMap[rid].send();

};


// Send a request to the server with xml data
AjaxEngine.sendXML = function(callback, method, xmldata ){

};

// Send a request by using the inputs of a form. This function will make either a 'POST' or 'GET' request based on the 
// value of the METHOD attribute of the form. The function will ignore the forms ACTION target and will use the gateway
// instead
AjaxEngine.sendForm = function(callback, formObj ){	
	
	var useDefaultRegMeth = !arguments[1];
	var f = formObj;
	var p = new Object();
	
	//Build Parameter list
	for(i=0; i < f.elements.length; i++){
		ffObj = f.elements[i];
		switch( ffObj.type ){
			case 'hidden':
			case 'text':
			case 'textarea':				
				p[ffObj.name] = encodeURIComponent(ffObj.value);
			break;
			case 'radio':
				for(j=0; j < ffObj.elements.length; j++){
					if( ffObj[j].checked ){
						p[ffObj.name] = ffObj[j].value;
						break;
					}
				}
			break;
			case 'checkbox':
				if(ffObj.length){
					var v = '';
					for(j=0; j < ffObj.elements.length; j++){
						if( ffObj[j].checked )
							v += (v.length) ? ','+ ffObj[j].value : ffObj[j].value;					
					}
					if( v.length )
						p[ffObj.name] = v;
				}else{
					if(ffObj.checked){
						p[ffObj.name] = ffObj.value;
					}
				}
			break;
			case 'select-one':				
				p[ffObj.name] = ffObj[ffObj.selectedIndex].value;
			break;		
		}
	}
	
	// create a unique ID for the request & add it to our parameter list.
	var rid = this.Helper.getUUID().toString();
	
	// create a new request object in the callmap
	this.callMap[rid] = new Request( rid, formObj.action, "", p, formObj.method );

	// Bind the callback function
	this.callMap[rid].setStatusCodeListener(200, callback);

	//Set the ready state 
	this.callMap[rid].setOnReadyStateHandler( function (){ AjaxEngine.listen( rid );} );
	
	// Execute the preHook
	if( this._preHook != null)
		this._preHook();
	
	// Send the request
	this.callMap[rid].send();	
};

//Listen to a request for a state changes
AjaxEngine.listen = function( requestID ){

	//Handle ready state triggers
	if( this.callMap[requestID].getReadyState() == http_COMPLETE ){
		if( this.callMap[requestID].hasStatusCodeListener( this.callMap[requestID].getHttpStatus() ) ){
			try{	
				if( this.debugMode )					
					this.Helper.displayDebugConsole(requestID, this.callMap[requestID].conn.responseText);

				// Execute the postHook
				if( this._postHook != null)
					this._postHook();
	
				eval( this.callMap[requestID].conn.responseText );						
			}catch( e ){
				//throw e;
				//Should we show Debug Info?
				if( this.debugMode ){
					dump(this.callMap[requestID]);
					this.Helper.displayDebugConsole(requestID, this.callMap[requestID].url +"<br>"+ this.callMap[requestID].conn.responseText);
				}else
					throw e;
			}
		}else if( this.debugMode ) {
			dump(this.callMap[requestID]);
			this.Helper.displayDebugConsole( requestID, this.callMap[requestID].url +"<br>"+ this.callMap[requestID].conn.responseText );
		}
	}
};


// Respond to the servers reponse
AjaxEngine.respond = function( requestID, requestObject ){
	var call = this.callMap[requestID];
	try{		
		call.httpStatusListeners[call.getHttpStatus()]( requestObject );			
	}catch( e ){
		throw e;
		if( this.debugMode )
			this.Helper.displayDebugConsole(requestID, call.http.responseText);
	}
};

// set a prehook to the call
AjaxEngine.setPreHook = function( ph ){ 
	if( typeof ph == 'function' ){
		this._preHook = ph;
	}else{ 
		throw "Prehook is not a valid function\n"+ ph;
	}
};	

// set a posthook to the call
AjaxEngine.setPostHook = function( ph ){ 
	if( typeof ph == 'function' ){
		this._postHook = ph;
	}else{ 
		throw "Posthook is not a valid function\n"+ ph;
	}
};

/**
 * This function returns a unique identifier
 * @return uuid a unique identifier
 */
AjaxEngine.Helper.getUUID = function(){
	var random = Math.floor(Math.random() * 10001);
    return "AE"+ (random.toString() + new Date().getTime()).toString();
}

/**
 * This function 'explodes' the parameters from an object to a url safe string
 * TODO: serialize paramObj using JSON
 * @return an ampersand delimeted query string
 */
AjaxEngine.Helper.explodeParams = function( paramObj ){
	var qs = "";
	if( typeof(paramObj) == 'object' ){
		for( key in paramObj ){
			qs += key +"="+ paramObj[key] +"&";
		}
	}else{
		qs = paramObj;
	}		
    return qs;
}


/**
 * This function builds a display console for debug or dump text
 * @param requestID  the id of the <see>Request</see> object making the call to display the debug info
 */ 
AjaxEngine.Helper.displayDebugConsole = function( requestID, debugText ){

	//Build the debug window
	var debugID = requestID;
	var debugHTML = "<span style='font:10pt verdana'><b>AjaxEngine Data Dump</b><br><span style='font:8pt verdana'>To remove this popup, turn off debugging info, by setting '<i>AjaxEngine.debugMode = false;</i>'.</span><br><hr><div style='width:100%;height:350px;overflow:auto;background-color:white;border:1px inset silver'>"+ debugText.replace(/;/g,';<br>') +"</div>" +
		 "<div align='right'><a href='#' onClick='document.getElementById(\""+ debugID +"\").style.display=\"none\";'>Close (X)</a></div>";
	var debugDiv = document.createElement('div');
	    debugDiv.id = debugID.toString();
		debugDiv.style.backgroundColor='aliceblue';
		debugDiv.style.border = '2px outset midnightblue';
		debugDiv.style.position = 'absolute';
		debugDiv.style.top = '80px';
		debugDiv.style.left = '10px';
		debugDiv.style.width = '650px';
		debugDiv.style.padding = '5px';
		debugDiv.style.zIndex='100';
		debugDiv.innerHTML = debugHTML;
		
	//Write the console to the screen and assume focus
	document.body.appendChild( debugDiv );
	
}

//Creates an request object that is used by the AjEngine 
function Request( requestID, gatewayURI, event, data, httpmethod ){
	
	/*
	 * Public Members
	 */
	this.method = httpmethod;  	            // HTTP method to use when making a request
	this.id = requestID;  		            // this Request object's unique id
	this.conn = this.getHttpConnection();   // an instance of the XMLHttpRequest Object
	this.url = gatewayURI +"?_requestID="+ requestID;	 // URL to call		
	this.data = AjaxEngine.Helper.explodeParams( data ); // the data that will be sent with any requests
	this.readyStateListeners = new Array(); // listeners that are binded to the xmlHttpRequest object onreadystate states
	this.httpStatusListeners = new Array(); // listeners that are binded to response http status codes 	
	this.url += (event.length > 0) ? "&event="+ event : "";
	/*
	 * This function sends the actual request to the server. It will send the request using the type defined
	 * in the <see>method</see> attribute
	 */
	this.send = function(){
		if( this.method == "POST"){	
			this.conn.open("POST", this.url, true);
			this.conn.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
			this.conn.send(this.data);
		}else{
			this.conn.open("GET", this.url +"&"+ this.data, true);
			this.conn.send(null);		
		}
	};
			
	/*
	 * This function sends the truth of whether or not there is a listener binded to an onreadystate state
	 * @param  readyState  the onreadystate state to check for a listener
	 */
	this.hasReadyStateListener = function ( readyState ){
		return this.readyStateListeners[readyState] ? true : false ;
	}
		
	/*
	 * This function sends the truth of whether or not there is a listener binded to an onreadystate state
	 * @param  statusCode  the http status code to check for a listener (ready states are described by the XMLhttpRequest Constants above)
	 */
	this.hasStatusCodeListener = function ( statusCode ){
		return this.httpStatusListeners[statusCode] ? true : false ;
	}
	
	/*
	 * This function loosely binds a listener to a response status code 
	 * @param  statusCode  the http status code that the listener will be binded too
	 * @param  listener    the method to execute when the returned status code is encountered on this objects requests
	 */
	this.setStatusCodeListener = function( statusCode, listener ){
		if( isNaN(statusCode) )
			throw "The status code '"+ statusCode +"' is not a valid HTTP status Code";
		if( typeof listener == "function" ){
			this.httpStatusListeners[statusCode] = listener;
		}else{ 
			alert(typeof listener);
			alert(listener.toString());
			throw "The status code trigger is not a valid function: Status Code: "+ statusCode;
		}
	};
	
	/*
	 * This function returns the listener binded to a response status code 
	 * @param  statusCode  the http status code that the listener will be binded too
	 * @return             the listener method or null if none
	 */
	this.getStatusCodeListener = function( statusCode ){
		if( isNaN(statusCode) || typeof this.httpStatusListeners[statusCode] == 'undefined' )
			return null;
		else 
			return this.httpStatusListeners[statusCode];
	};
	
	/*
	 * This function loosely binds a listener to the xmlHttpRequest Objects onreadystate states
	 * @param  readyState  the onreadystate state that the listener will be binded too
	 * @param  listener    the method to execute when the onreadystate state is encountered on this objects requests
	 */
	this.setReadyStateListener = function( readyState, trigger ){
		if( isNaN(readyState) || readyState < 0 || reaydState > 3 )
			throw "The ready state '"+ readyState +"' is not a valid ready state.";
		if( typeof trigger == "function" ){
			this.readyStateListeners[readyState] = trigger;
		}else{ 
			throw "The ready state trigger is not a valid function: Ready State: "+ readyState;
		}
	};
	
	/*
	 * This function returns the listener binded to a response status code 
	 * @param  statusCode  the http status code that the listener will be binded too
	 * @return             the listener method or null if none
	 */
	this.getReadyStateListener = function( readyState ){
		if( isNaN(readyState) || typeof self.readyStateListeners[readyState] == 'undefined' )
			return null;
		else 
			return this.readyStateListeners[readyState];
	};
	
	/*
	 * This function sets the method that should be executed when the onreadystate method of the http request connection
	 * is fired. 
	 * @param  handler the method to set as the onreadystate function
	 */
	this.setOnReadyStateHandler = function( handler ){
		if( typeof handler == "function" ){
			this.conn.onreadystatechange = handler;
		}else{ 
			throw "The ready state handler is not a valid function:";
		}
	}
	
	/*
	 * This function gets the current ready state code
	 */
	this.getReadyState = function(){
		return this.conn.readyState;
	}

	/*
	 * This function gets the current http status
	 */
	this.getHttpStatus = function(){
		return this.conn.status;
	}

}

/**
 * This function returns an instance of a connection object for the engine. It will return 
 * either a XMLHttpRequestObj or an iframe document object. The methods used by the request
 * have been prototyped to the 
 * @return  an instance of XMLHttpRequest object
 */
Request.prototype.getHttpConnection = function(){
	var conn = null;
	if (window.XMLHttpRequest){
		conn = new XMLHttpRequest();
	}else if(window.ActiveXObject){
		try{
			conn = new ActiveXObject("Msxml2.XMLHTTP");
		}catch( e ){
			try{
				conn = new ActiveXObject("Microsoft.XMLHTTP");
			}catch( E ){
				//revert to hidden iframe mode;
			}
		}
	}
	return conn;
}


/**
 * This extends the String object with the functionality to trim whitespace from the begining and ending of a string
 * @param str  the string to remove whitespace from
 * @return     the string with whitespace removed from the begining and end of <see>str</see>
 */
String.prototype.trim = function ( str ){
	if(arguments[0] && str.length){
		var wsb = /^(\s)*/;
		var wse = /(\s)*$/;
		str = str.replace(wsb,'');
		return str.length ? str.replace(wse,'') : str;			
	}else
		return '';
}
