/******************************************************
	File: XHR
	
	About:
	Functions for generating and handling ajax (XMLHttpRequest) requests	
	
	Notes:
		- Handler functions and targets are looked up in the following priority order: server-defined, local, global
		- server defined handler functions are set by adding a HTTP header of "XHR-handler" in the server response
		- server defined targets are set by adding a HTTP header of "XHR-target" in the server response
		- Handler functions should always expect 3 parameters: request object, target, request id
		- 3 log types are added (disabled). To enable logging, enable 'XHR-requests','XHR-errors',XHR-events' using <Logs.enableLogType>
		
	"Send Objects":
		functions that refer to a "send object" require an object with the following properties
		
		id - value
		lock - (optional) boolean, default=false 
		asynch - (optional) boolean, make asynchronous call, default=true (see XMLHttpRequest object)
		username - (optional) value (see XMLHttpRequest object)
		password - (optional) value (see XMLHttpRequest object)
		
	example:
		{
			id:"myId",
			lock:true,
			asynch:true,
			username:,
			password:value
		}
		
	requires: 
		JSManager
	
*********************************************************/
/*
	Class: XHR
	Core Ajax Object
	
*/
var XHR=new Object();
Class.inherits(XHR,{
	init:function(){
		this.name="XHR";
		this.locked=false;
		this.lockedId=null;
		this.requests=new Hash();
		this.pendingRequests = new Array();
		this.activeRequests = new Hash();
		this.activeRequestCount=0;
		this.maxActiveRequests=-1;				
		this.globalParams = new Hash();
		this.globalHeaders=new Hash();
		this.globalTimeout=null;
		this.globalHandler=this.pasteImport;
		this.globalTarget=null;
		this.rTypes=new Hash();
		this.addRequestType('form','application/x-www-form-urlencoded',this.buildQuery);
		this.addRequestType('xml','text/xml');
		this.addRequestType('text','text/plain');
		this.addRequestType('html','text/html');
		this.unsupportedFunc=function(){alert("Your browser does not support dynamic data loading")}
		this.doOtherStatusCode=function(id){this.fail(id,"HandleReadyStateDone\n");}
		this.fail=this.defaultError;
	},
/*
	Function: addRequest
	Creates a new <XHR.httpRequest> object
	
	parameters:
		id - a unique id to reference the request by
		url - the URL the request will be submitting to
		opts - see <XHR.httpRequest> for the description of options
*/
	addRequest: function(id,url,opts){
		this.requests[id]=new XHR.httpRequest(url,opts);
		Log('XHR-events',[id,'Add Request']);
	},
/*
	function: addGlobalParam
	adds a URL Parameter to the global XHR scope (added to all requests)
	
	parameters:
		name - the name of the parameter
		val - the value of the parameter
*/
	addGlobalParam: function(name,val){
		this.globalParams[name]=val;
		Log('XHR-events',['Global','Add Global Param: '+name]);
	},
/*
	function: addGlobalHeader
	adds a HTTP Header to the global XHR scope (added to all requests)
	
	parameters:
		name - the name of the parameter
		val - the value of the parameter
*/
	addGlobalHeader: function(name,val){
		this.globalHeaders[name]=val;
		Log('XHR-events',['Global','Add Global Header: '+name]);
	},
/*
	function: setGlobalHandler
	sets the global handler function (default is <pasteImport>).
	
	parameters:
		name - the name of the parameter
		val - the value of the parameter
*/
	setGlobalHandler: function(f){
		this.globalHandler=f;
		Log('XHR-events',['Global','Add Global Handler']);
	},
/*
	function: setGlobalTarget
	sets the global target (passed to the handler function, should be an HTML element pointer or ID)
	
	parameters:
		name - the name of the parameter
		val - the value of the parameter
*/
	setGlobalTarget: function(t){
		this.globalTarget=t;
		Log('XHR-events',['Global','Add Global Target']);
	},
/*
	function: setMaxActiveRequests
	sets the maximum number of requests that can be opened at a time.  Requests become automatically locked and queued when met or exceeded. (-1=default unlimited)
	
	parameters:
		v - the max number to be opened
*/
	setMaxActiveRequests: function(v){
		this.maxActiveRequests=v;
		Log('XHR-events',['Global','Set Max Requests: '+v]);
	},
/*
	function: queueRequest
	(*internal*) Queues a request when XHR is locked
	
	parameters:
		obj - a "send object"
*/	
	queueRequest: function(obj){
		this.pendingRequests.push(obj);
		Log('XHR-events',[obj.id,'Queue Request']);
	},
/*
	function: send
	takes an existing request and sends it to the server
	
	parameters:
		obj - a "send object"
*/	
	send: function(obj){
		this.makeHttpCall(obj);
	},

/* 
	function: quickSend
	generates a request object and sends it at the same time

	parameters:
		obj - an object with the following properties
		
	Object Properties:
>	{
>		id:value, (required)
>		url:url to send to, (required)
>		lock: Boolean (optional, default=false)
>		data:data (post/xml) or query string (optional),
>		asynch:Boolean (optional, default=true)
>		username:value (optional),
>		password:value (optional),
>		method:GET/POST/HEAD, (optional, default=GET)	
>		handler: handler function (optional)
>		target: target ID/reference (optional),
>		timeout:time in ms (optional),
>		requestType: xml/text/html etc (optional, default=form)
>	}
*/		
	quickSend: function(obj){
				var o=new Object();
				if(obj.method){o.method=obj.method}
				if(obj.requestType){o.requestType=obj.requestType}
				if(obj.timeout){o.timeout=obj.timeout}
				if(obj.target){o.target=obj.target}
				if(obj.handler){o.handler=obj.handler}
				if(obj.data){o.data=obj.data}
				this.addRequest(obj.id,obj.url,o);				
				var o2=new Object();
				o2.id=obj.id;
				o2.lock=(isDefined(obj.lock)?obj.lock:false);
				o2.asynch=(isDefined(obj.asynch)?obj.asynch:true);
				o2.username=obj.username||null;
				o2.password=obj.password||null;
				this.makeHttpCall(o2);
			},
/*
	function: makeHttpCall
	(*internal*) builds the XHMHttpRequest call and sends it to the server 
	
	parameters:
		obj - a "send object"
*/	
	 makeHttpCall: function(obj){
	 			var asynch=(isDefined(obj.asynch)?obj.asynch:true);
				var lock=(isDefined(obj.lock)?obj.lock:false);
				var _this=this;
				if(this.locked){
					this.queueRequest(obj);
					return
				}
				var req=this.openHttpConn();
				if(!req){
					this.unsupportedFunc(); 
					return
				}
				else{
					this.activeRequests[obj.id]=req;
				}
				this.activeRequests[obj.id].onreadystatechange=function(){
					_this.handleReturn.call(_this,obj.id)
				}
				var q='';
				if(this.requests[obj.id].data){					
					q=this.requests[obj.id].data;
				}
				else if(this.rTypes[this.requests[obj.id].requestType].typeProcessor!=null){					
					q=this.rTypes[this.requests[obj.id].requestType].typeProcessor.call(this,obj.id);
				}	
				
				var url=this.requests[obj.id].url;
				this.requests[obj.id].sentVal=q;
				if((isValidText(q))&&(this.requests[obj.id].method=="GET")){
					url=url.addParam(q);
					q="";
				}					
				if(obj.username){
					this.activeRequests[obj.id].open(this.requests[obj.id].method,url,asynch);
				}
				else{
					this.activeRequests[obj.id].open(this.requests[obj.id].method,url,asynch,obj.username,obj.password);
				}
				if(this.rTypes[this.requests[obj.id].requestType]!=null){
					this.activeRequests[obj.id].setRequestHeader('Content-type',this.rTypes[this.requests[obj.id].requestType].typeEncoding);
				}
				this.globalHeaders.forEach(function(val,key){
					_this.activeRequests[obj.id].setRequestHeader(key,val);
				});				
				this.requests[obj.id].headers.forEach(function(val,key){
					_this.activeRequests[obj.id].setRequestHeader(key,val);
				});				
				this.activeRequestCount++;	
				if((obj.lock)||((this.maxActiveRequests>0)&&(this.activeRequestCount>=this.maxActiveRequests))){
					this.lockRequests(obj.id);
				}
				this.startTimer(obj.id);
				this.activeRequests[obj.id].send(q);
			
				var logModifiers=[];
				//logModifiers[0]="Method: "+this.requests[obj.id].method+_BR;		
				//logModifiers[1]="Request Type: "+this.requests[obj.id].requestType+_BR;
				//logModifiers[2]="target: "+this.requests[obj.id].target+_BR;
				//logModifiers[3]="headers: "+this.requests[obj.id].headers.join(' ; ')+_BR;
				//logModifiers[4]="timeout: "+this.requests[obj.id].timeout+_BR;
				//Log('XHR-requests',[obj.id,'start','<xmp>'+this.requests[obj.id].sentVal+'</xmp>',logModifiers.join(_BR+_BR),obj.lock]);
			},
/*
	function: openHttpConn
	(*internal*) gets the actual XMLHttpRequest object
*/	
	openHttpConn: function(){
						if(window.XMLHttpRequest) {
							return (new XMLHttpRequest());
						}
						else if (window.ActiveXObject){
							try {
					 			return (new ActiveXObject("Msxml2.XMLHTTP"));
								
					 		} catch (e) {
					  			try {
					   				return (new ActiveXObject("Microsoft.XMLHTTP"));
								} catch (E) {
									return false
								}
							}
						}
						return false;
					},
/*
	function: closeRequest
	Closes (aborts) an request in progress (see XMLHttpRequest.abort() documentation)
		
	parameters:
		id - the request ID to be aborted
		code - (optional) a code/reason to be logged with the closing
*/	
	closeRequest: function(id,code){
				if(this.activeRequests[id]){
					this.activeRequests[id].abort();
					if((this.fail!=null)&&(code)){
						this.fail(code,this.requests[id]);
					}
					Log('XHR-events',[id,'Closing Request: '+code]);
				}
			},
/*
	function: deleteRequest
	deletes a request object
		
	parameters:
		id - the request ID to be removed

*/	
	deleteRequest: function(id){
			this.requests[id]=void 0;
			delete this.requests[id];
			Log('XHR-events',[id,'Delete request']);
		},	
/*
	function: startTimer
	(*internal*) starts a timer that will abort the active request when it expires (if the request is not completed)

	parameters:
		id - the id of the request object to track
*/	
	startTimer: function(id){
			var inter=((this.globalTimeout!=null)?((this.requests[id].timeout!=null)?this.requests[id].timeout:this.globalTimeout):null);
			if(inter!=null){
				this.requests[id].intervalCode=setTimeout(function(){XHR.requestTimeOutFail(id)},inter);
			}
		},
/*
	function: requestTimeOutFail
	(*internal*) closes an active request if it times out
*/	
	requestTimeOutFail: function(id){
			this.closeRequest(id,"timeout");
			this.requests[id].intervalCode=null;
		},		
/*
	function: lockRequests
	Locks requests with a request ID.  When that ID returns it will unlock.
	
	parameters:
		id - the id of the request object requesting locking status (fails if already locked)
*/	
	lockRequests: function(id){
				if(!this.locked){
					this.locked=true;
					this.lockedId=id;
					Log('XHR-events',[id,'Lock']);
				}
			},			
/*
	function: unlockRequests
	Unlocks requests
	
	parameters:
		id - the id of the request object that is requesting to unlock (fails if it is not the locking id)
		send - boolean, whether to send pending/queued requests (true if auto-locked)
*/	
	unlockRequests: function(id,send){
				if((id==this.lockedId)||(!this.activeRequests[id])){
					this.locked=false;
					this.lockedId=null;
					Log('XHR-events',[id,'Unlock']);
					if(send){
						this.sendPendingRequest();
					}						
				}
			},	
/*
	function: sendPendingRequest
	(*internal*) pulls the next object from the pending requests and sends it.
*/	
	sendPendingRequest: function(){
				if(this.pendingRequests.length>0){
					var parms=this.pendingRequests.shift();	
					Log('XHR-events',[parms.id,'DeQueue Pending Request']);				
					this.makeHttpCall(parms);
				}
			},					
/*
	function: buildQuery
	(*internal*) builds a query string (without the ?) from the local and global parameters of the request object
*/	
	buildQuery: function(id){
			var qstr="";
			this.globalParams.forEach(function(val,key){
				qstr=qstr.addParam(key,val);
			});
			this.requests[id].params.forEach(function(val,key){
				qstr=qstr.addParam(key,val);
			});
			qstr=qstr.substring(1);
			return qstr;				
		},
/*
	function: addRequestType
	adds a request type (such as "form","xml") which determines how to handle the data/request before it is sent
	
	parameters:
		typeName - the name to be used to reference the type
		typeEncoding - the content-type header value
		typeProcessor - (optional) a function to be used to process/create the data for the request (<buildQuery> used for "GET").  Should take a parameter of the request ID and should return the data to be submitted with the request.  This gets called *only* if the requests data property is empty.
*/			
	addRequestType: function(typeName,typeEncoding,typeProcessor){
			this.rTypes[typeName]=new Object();
			this.rTypes[typeName].typeEncoding=typeEncoding;
			this.rTypes[typeName].typeProcessor=typeProcessor;
		},
/*
	function: handleReturn
	(*internal*) Monitors the state of the request and if readyState=4 and status=0,200-299 passes the request object to the handler function. Set doOtherStatusCode to handle status codes outside of the "good" range
*/	
	handleReturn: function(id){	
			if(this.activeRequests[id].readyState==4){
				this.requests[id].response=this.activeRequests[id];
//				alert(this.activeRequests[id].responseText);
				var st=this.requests[id].response.status;				
				delete this.activeRequests[id];
				Log('XHR-requests',[id,'finish','<xmp>'+this.requests[id].response.responseText+'</xmp>',this.requests[id].response.getAllResponseHeaders().split(/\n/g).join(_BR),null]);
				if(st==0||(st>=200&&st<300)){
					var req=this.requests[id].response;
					var sHand=req.getResponseHeader('XHR-handler');
					var sTarg=req.getResponseHeader('XHR-target');
					var jsHand=(isValidText(sHand)?sHand:((this.requests[id].handler!=null)?this.requests[id].handler:this.globalHandler));
					var targ=(isValidText(sTarg)?sTarg:((this.requests[id].target!=null)?this.requests[id].target:this.globalTarget));
					JsManager.runFunction(jsHand,[req,targ,id,this]);
					this.unlockRequests(id,true);						
				}
				else{
					this.doOtherStatusCode(id);
				}
			}			
		},
/*
	function: defaultError
	(*internal*) the default error handling function.  Just logs the request and an error message.
*/		
	defaultError: function(id,mess){
			Log('XHR-errors',[id,mess,this.requests[id].response.readyState,this.requests[id].response.status,this.requests[id].response.getAllResponseHeaders(),this.requests[id].response.responseText]);
		},	
/*
	function: pasteImport
	(*internal*) default global handler, just pastes the responseText into the target 
*/	
	pasteImport: function(cont,targ){
			targ=$(targ);
			if(targ!=null){
				setHTML(targ,cont.responseText);					
			}			
		}
});



//
// opts: {optCode:value, optCode:value}
// optCodes: method
// 			 requestType
// 			 timeout
// 			 target
// 			 handler
//			 data
// use addHeader, addData, and addParam to set those
//  Handler functions should always expect 3 parameters: request object, target, id
XHR.httpRequest=Class.create();
/*
	Class: XHR.httpRequest
	a request object used by <XHR>
*/
Class.inherits(XHR.httpRequest.prototype,{
/*
	function: init
	
	parameters:
		url - the URL for the request
		opts - (optional) an object with the following properties (all optional)
		
>	{
>		method: GET/POST/HEAD (case sensitive, default=GET)
>		requestType: form/xml etc (case sensitive, see <XHR.setRequestType>)
> 		timeout: the time in ms to timeout the request when sent
>		target: the target to be passed to the handler function
> 		handler: the function to be used to process the return
>		data: the data to be sent with the request
>	}
*/
	init:function(url,opts){
		this.url=url;
		Class.inherits(this,{
			params:new Hash(),
			method:'GET',	
			requestType:'form',
			target:null,
			handler:null,
			headers:new Array(),
			timeout:null,
			data:null,
			sentVal:null,
			intervalCode:null,
			connection:null,
			response:null
		});
		if(opts){
			Class.inherits(this,opts,true);
		}
	},
/*
	function: addHeader
	adds a HTTP Header to the request
	
	parameters:
		name - the name of the parameter
		val - the value of the parameter
*/
	addHeader: function(lbl,val){this.headers[lbl]=val;},
/*
	function: addParameter
	adds a (url) parameter to the request
	
	parameters:
		name - the name of the parameter
		val - the value of the parameter
*/
	addParam: function(param,val){this.params[param]=val},
/*
	function: setUrl
	sets the URL of the request
	
	parameters:
		url - the url to send the request to
*/
	setUrl: function(url){this.url=url},
/*
	function: setMethod
	Sets the request method (get,post,head)
	
	parameters:
		meth - the new Method (case insensitive)
*/
	setMethod: function(meth){this.method=meth.toUpperCase();},
/*
	function: setRequestType
	Sets the request type (form, xml, etc)
	
	parameters:
		rType - the new request type
*/
	setRequestType: function(rType){this.requestType=rType},
/*
	function: setTarget
	Sets the target to be passed to the handler function
	
	parameters:
		t - target id or DOM pointer
*/
	setTarget: function(t){this.target=t},
/*
	function: setHandler
	Sets the handler for the request
	
	parameters:
		h - the handler function
*/
	setHandler: function(h){this.handler=h},
/*
	function: setRequestTimeout
	Sets the timeout for the request
	
	parameters:
		t - time in ms
*/
	setRequestTimeout: function(t){this.timeout=t},
/*
	function: setData
	Sets the data/param string to be submitted with the request
	
	parameters:
		d - the data/param string to be submitted with the request
*/
	setData: function(d){this.data=d}
});


XHR.init();

Logs.addLogType('XHR-requests',false,false,['Id','Action','Data','Modifiers','Lock']);
Logs.addLogType('XHR-errors',false,false,['ID','Message','Ready State','Status Code','Headers','Response Text']);
Logs.addLogType('XHR-events',false,false,['ID','Message']);
XHR.addGlobalHeader('XHR','TRUE');