/*
 *  File:     pexodrpc.js
 *
 *  Usage:    Initialization:
 *            var oRPC_test = new AsynchRPC();
 *            
 *            Testing:
 *            oRPC_test.testasynch( sServerURL, sTargetId );
 *            where sServerURL is a bogus server, although it should exist as a 
 *            server-side script or web page, and sTargetId is a valid element id
 *            on the calling page. This allows testing of the RPC component,
 *            independent of the server.
 *            
 *            Usage:
 *            oRPC.asynchfetchXML( sServerURL, "FirstRow" );
 *            where "FirstRow" is the id of a container element in the web page
 *            
 *            Notes:
 *            The intended usage is to provide a mechanism to load content chunks that
 *            are not natively supported by the website infrastucture. This entails
 *            instantiating an instance, making a call to some server-side service, and
 *            having the result be parsed into a container established in the page
 *            template.
 *            
 *            It is worthwhile instantiating a new instance of AsynchRPC for each
 *            request that you want to make on a page. This allows the browser to achieve
 *            whatever parallelism (in handling requests) it is able to. (FF 1.5 does one
 *            request at a time and establishes a queue of requests, IE 6 can do 2 at
 *            a time.)
 *            
 *            Note: pTranscribeXML() cannot transcribe style rules within <style>
 *            tags. At present, IE throws an exception. Todo, this would be a good
 *            thing to fix.
 *            
 *            
 *            See publicrpc.php for more details.
 *
 *  Note:     If you are looking to test stuff in the RPC component, there are
 *            various messages that you can turn on. Check the code below.
 *            
 *  Note:     The results can turn debugging on via:
 *            mp_bDebug = true;
 *            
 *  Note:     If you pass something like <div id="<![CDATA[myid]]>" which is to
 *            say, use a CDATA inside the attributes of a tag, in other words,
 *            pass malformed XML, a browser-specific scenario unfolds:
 *            In IE, responseXML.documentElement is NULL, and in FF the XML 
 *            preparser will choke and refuse to pass along the XML DOM 
 *            structure, so it's impossible to do the element-by-element parse
 *            of the fetched results. The workaround implemented here is to
 *            use the mp_Request.responseText, clean it up, and use innerHTML
 *            to stuff it into the target element. This means that any 
 *            'immediate-execution' javascript will not be executed. And this 
 *            means that the results can not turn debugging on. JUST WHEN YOU
 *            MOST NEED IT!
 *
 *            
 *            
 */

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// this takes a javascript struct, of a very specific layout
// and parses it into an RPC call, the intent is to provide
// a means of passing RPC calls out through an XML channel 
// without being screwed up the the ampersand debacle that is XML
// 
// Here is a sample struct:
//
// var oRPC_newtitlelist = 
// 	{
// 		"RPCClientInstance"		: new AsynchRPC()
// 		, "RPCServerBaseURL"	: sRPCServerBaseURL
// 		, "ServerScript"		: "publicrpc.php"
// 		, "ServiceName"			: "titlebriefs"
// 		, "Command"				: "newtitlebriefs"
// 		, "Debug"				: ""					// optional
// 		, "ResultTarget"		: "container_rightcolumn_newtitles"
// 		, "ServiceParms"		:
// 			[
// 				{"template"	: "svc_newtitlelist.tmpl"}
//				// add more service-specific parms as nesc.
// 			]
// 	};
// 
function RPCCallWrapper( oParms )
{
	var sAmp = String.fromCharCode( 38 );
	var sJoint = "";
	var sServerURL = oParms.RPCServerBaseURL + oParms.ServerScript;
	var sServerParmList = "servicename=" + oParms.ServiceName + sAmp + "command=" + oParms.Command;
	var sServiceParmList = "";
	var aServiceParm, sServerParmName;
	if( null != oParms.Debug )
	{
		sServerParmList += sAmp + "debug=" + oParms.Debug;
	}
	for( aServiceParm in oParms["ServiceParms"] )
	{
		for( sServerParmName in oParms["ServiceParms"][aServiceParm] )
		{
			sServiceParmList += sAmp + sServerParmName +"="+ oParms["ServiceParms"][aServiceParm][sServerParmName]
		}
	}
	oParms.RPCClientInstance.asynchfetchXML( sServerURL +"?"+ sServerParmList + sServiceParmList, oParms.ResultTarget );
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function AsynchRPC_Exception( sMsg )
{
	this.message = sMsg;
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// this is a hack to allow various failures in handling 
// the XML response to fail-over to the text response
function AsynchRPC_ParseException( sMsg, bSwitchToTextDocument )
{
	this.message = sMsg;
	this.bSwitchToText = bSwitchToTextDocument;
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function AsynchRPC()
{
	var self = this;	// 'self' provides the context inside public members
	var mp_Request;
	var mp_Messages = new Array();
	var mp_Errors = new Array();
	var mp_bDebug = false;

	//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
	// IE XMLHttp versions
	var aIEVersions = 
		[
			"MSXML2.XMLHttp.5.0"
			, "MSXML2.XMLHttp.4.0"
			, "MSXML2.XMLHttp.3.0"
			, "MSXML2.XMLHttp"
			, "Microsoft.XMLHttp"
		];
	// create request object
	if( window.XMLHttpRequest )
	{
		mp_Request = new XMLHttpRequest();
		mp_Messages[ mp_Messages.length ] = "XMLHttp object is 'XMLHttpRequest'";
	}
	else if( window.ActiveXObject )
	{
		for( var i=0; i<aIEVersions.length; i++ )
		{
			try
			{
				mp_Request = new ActiveXObject( aIEVersions[ i ] );
				mp_Messages[ mp_Messages.length ] = "XMLHttp object is '" + aIEVersions[ i ] + "'";
				break;
			}
			catch( e )
			{
				// do nothing
			}
		}
	}
	if( !mp_Request )
	{
		mp_Errors[ mp_Errors.length ] = "No available XML RPC\n";
	}

	//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
	//
	// PRIVATE METHODS:
	//
	//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

	//----------------------------------------------------
	// replace contents of target ID with passed in text, 
	// handy for debugging
	pTextLoader = function( sTargetElementID, sText )
	{
		//alert(sText);
		var oTargetElement;
		oTargetElement = document.getElementById( sTargetElementID );
		if( !oTargetElement ) throw new AsynchRPC_Exception( "AsynchRPC::pTextLoader(): Target element (id='" + sTargetElementID + "') does not exist" );
		// empty target element
		for( var i=0; i<oTargetElement.childNodes.length; i++)
		{
			oTargetElement.removeChild( oTargetElement.childNodes.item(i) );
		}
		// signal something...
		oTargetElement.appendChild( document.createTextNode( sText ) );
	}

	//----------------------------------------------------
	// strip XML artefacts from sText and stuff the string
	// into the target element
	pCleanAndStuffText = function( oTarget, sText )
	{
		var rOpen = new RegExp( "<![\[]CDATA[\[]", "gim" );
		var rClse = new RegExp( "\]\]>", "gim" );
		var sClean = sText;
		sClean = sClean.replace( /<\?xml[^>]*>/, "" );
		sClean = sClean.replace( rOpen, "" );
		sClean = sClean.replace( rClse, "" );
		oTarget.innerHTML = sClean;

		mp_Messages[ mp_Messages.length ] = "pCleanAndStuffText() stuffed the responseText into '" + oTarget.id + "'";
		mp_Messages[ mp_Messages.length ] = "pCleanAndStuffText() responseText:\n" + sClean;
	}

	//----------------------------------------------------
	// if the results are a PHP error, put up an alert message
	// and indicate that there is nothing further to be done,
	// otherwise, if it's not a PHP error return false
	pHandlePHPError = function( sText )
	{
		var rParseError = new RegExp( "Parse error", "gim" );
		var rTag = new RegExp( "<[^>]*.", "gim" );
		var sClean = "";
		if( 0 < sText.search( rParseError ) )
		{
			sClean = sText.replace( rTag, "" );
			mp_Messages[ mp_Messages.length ] = "pHandlePHPError() detected a PHP error: " + sClean;
			alert( sClean );
			return true;
		}
		return false;		
	}

	//----------------------------------------------------
	// this does a depth first traversal of the passed
	// XHTML tree and stuffs it into the element id=sTargetElementID
	function pTranscribeXML( oTargetElement, oHTMLTree )
	{
		var NewElement = null;
		if( null == oHTMLTree )
		{
			mp_Messages[ mp_Messages.length ] = "pTranscribeXML() error: NULL HTML Tree";
			return;
		}

		try
		{
			switch( oHTMLTree.nodeType )
			{
			default:
				mp_Messages[ mp_Messages.length ] = "pTranscribeXML() error: Unrecognized node type: " + oHTMLTree.nodeType;
				// create a node so that the script can carry on, and not fail because
				// 'NewElement' is null ... have to trust that when a thing isn't 
				// transcribed as expected you will use eg FireFox DOM inspector to
				// see the comment node
				NewElement = document.createComment( "pTranscribeXML() error: Unrecognized node type: " + oHTMLTree.nodeType );
				break;

			case 8:
				mp_Messages[ mp_Messages.length ] = "pTranscribeXML() This is a comment node. text='" + oHTMLTree.data + "'";
				NewElement = document.createComment( oHTMLTree.data );
				// and since there's no nice way to inspect the DOM, there's little point
				// in constructing comments, so just return
				//return;
				break;

			case 4:
				mp_Messages[ mp_Messages.length ] = "pTranscribeXML() CDATA Section='" + oHTMLTree.data + "'";
				// passed CDATA section, can't tell if it is ASCII or HTML, or even XHTML, 
				// so just stuff the data into the parent node - which might cause a
				// problem if there is other markup in the parent

				// TODO, resolve CDATA handling...
				// this works... but have to keep calling the parser ...
				// the return call is wrong
				//oTargetElement.innerHTML = oHTMLTree.data;
				//return

				NewElement = document.createTextNode( oHTMLTree.data );
				break;

			case 3:
				mp_Messages[ mp_Messages.length ] = "pTranscribeXML() TEXTNODE='" + oHTMLTree.data + "'";
				NewElement = document.createTextNode( oHTMLTree.data );
				break;

			case 7:
				// processing instruction, hopefully embedded javascript
				mp_Messages[ mp_Messages.length ] = "pTranscribeXML() PROCESSINGINSTRUCTION: target='" + oHTMLTree.target + "' data='" + oHTMLTree.data + "'";
				if( "javascript" == oHTMLTree.target.toLowerCase() )
				{
					eval( oHTMLTree.data );
				}
				else
				{
					mp_Messages[ mp_Messages.length ] = "pTranscribeXML() Unrecognized Instruction Processor: '" + oHTMLTree.target + "'";
				}
				// there is no node to process, so bail out here
				return;
				break;

			case 1:
				var sNewElementTagName = oHTMLTree.tagName.toLowerCase();
				mp_Messages[ mp_Messages.length ] = "pTranscribeXML() This is an element node. tagName='" + sNewElementTagName + "'";
				// FF can choke on the XML parse, but it doesn't throw an exception, 
				// instead it returns a bunch of XML containing the error message
				// and this is the name of the tag that FF uses to contain the error message
				if( sNewElementTagName == "parsererror" )
				{
					mp_Messages[ mp_Messages.length ] = "pTranscribeXML() Is abandoning the parse attempt";
					return false;
				}
				NewElement = document.createElement( sNewElementTagName );

				// deal with any attributes, by copying each one over
				var Att;
				var sAttName;
				var AttributeNodeList = oHTMLTree.attributes;
				for( var j=0; j<AttributeNodeList.length; j++ )
				{
					Att = AttributeNodeList.item(j);
					// have to handle events special for variation in browsers
					sAttName = Att.name.toLowerCase();
					if( "on" == sAttName.substring( 0, 2) )
					{
						mp_Messages[ mp_Messages.length ] = "Event Handler: Attribute #" + j + "='" + Att.name + "' \nvalue:\n" + Att.value;
						dputil_AddEventHandler( NewElement, sAttName.substring( 2, sAttName.length ), Att.value );
					}
					else if( "style" == sAttName )
					{
						// this is a way to copy the complete inline style info in so that IE gets it
						mp_Messages[ mp_Messages.length ] = "Style: Attribute #" + j + "='" + Att.name + "'";
						NewElement.style.cssText = Att.value;
					}
					else if( "class" == sAttName )
					{
						mp_Messages[ mp_Messages.length ] = "Class: Attribute #" + j + "='" + Att.name + "'";
						// this is for depricated FF
						NewElement.setAttribute( Att.name, Att.value );
						// this is spec-correct (IE)
						NewElement.setAttribute( "className", Att.value );
					}
					else
					{
						mp_Messages[ mp_Messages.length ] = "Attribute #" + j + "='" + Att.name + "'";
						NewElement.setAttribute( Att.name, Att.value );
					}
				}

				if( "script" == sNewElementTagName && oHTMLTree.hasChildNodes() )
				{
					// this is a script element with text contained within it,
					// so have to stuff that text directly without allowing 
					// pTranscribeXML() to turn that text into a text node
					// and make that a child of the script node
					var ChildNodeList = oHTMLTree.childNodes;
					var Item;
					var text = "";
					for( var i=0; i<ChildNodeList.length; i++)
					{
						Item = ChildNodeList.item(i);
						text = text + Item.data;
					}
					NewElement.text = text;
					oTargetElement.appendChild( NewElement );
					return;
				}
				else if( "style" == sNewElementTagName && oHTMLTree.hasChildNodes() )
				{

					throw( new AsynchRPC_Exception( "pTranscribeXML() can not transcribe embedded style sheets - place your style information in an external style sheet" ) );
				}
				break;
			}

			// attach the copied node, along with the copied attributes
			oTargetElement.appendChild( NewElement );
			// do the children of this node
			if( oHTMLTree.hasChildNodes() )
			{
				var ChildNodeList = oHTMLTree.childNodes;
				var Item;
				for( var i=0; i<ChildNodeList.length; i++)
				{
					Item = ChildNodeList.item(i);
					pTranscribeXML( NewElement, Item );
				}
			}
			else
			{
				// there are no child nodes, so do nothing
			}
		}
		catch( e )
		{
			var sMsg = "";
			sMsg = sMsg + "EXCEPTION in pTranscribeXML():\n"
			sMsg = sMsg + "Exception description: " + e.description + "\n";
			sMsg = sMsg + "oTargetElement.nodeType: " + oTargetElement.nodeType + "\n";
			sMsg = sMsg + "oTargetElement.tagName: " + oTargetElement.tagName + "\n";
			sMsg = sMsg + "NewElement.nodeType: " + NewElement.nodeType + "\n";
			sMsg = sMsg + "NewElement.tagName: " + NewElement.tagName + "\n";
			sMsg = sMsg + "NewElement.data: " + NewElement.data + "\n";
			for( var sMsgId in mp_Messages )
			{
				sMsg = sMsg + "Messages[" + sMsgId + "]: " + mp_Messages[sMsgId] + "\n";
			}
			alert( sMsg );
		}
		return true;
	}


	//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
	//
	// PRIVILEGED METHODS:
	//
	//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

	//----------------------------------------------------
	// show all the message strings in the message cache
	this.AlertMessages = function()
	{
		var sMsg = "";
		sMsg = sMsg + "mp_Messages: \n";
		for( var sMsgId in mp_Messages )
		{
			sMsg = sMsg + "Messages[" + sMsgId + "]: " + mp_Messages[sMsgId] + "\n";
		}
		alert( sMsg );
	}

	//----------------------------------------------------
	// for testing, reduced functionality
	this.testasynch = function( sServerURL, sTargetId )
	{
		pTextLoader( sTargetId, "LOADING ..." );
		if( !mp_Request ) throw new AsynchRPC_Exception( "AsynchRPC::testasynch(): Your browser doesn't support an XML HTTP Request" );
		mp_Request.onreadystatechange = function()
		{
			if( 4 == mp_Request.readyState )
			{
				alert( "state:"+mp_Request.readyState + " status:" + mp_Request.status );
				switch( mp_Request.status )
				{
				case 404:
					alert( "bad server URL" );
					break;

				case 200:
					pTextLoader( sTargetId, "LOADED" );
					break;
				};
			}
		};
		mp_Request.open( "GET", sServerURL, true );
		mp_Request.send( null );
	}

	//----------------------------------------------------
	// make request, copy results to target container
	this.asynchfetchXML = function( sServerURL, sTargetElementID )
	{
		var oTargetElement;

		if( !mp_Request ) throw new AsynchRPC_Exception( "AsynchRPC::asynchfetchXML(): Your browser doesn't support an XML HTTP Request" );
		// this also tests that the target id exists
		//pTextLoader( sTargetElementID, "LOADING ..." );

		mp_Request.onreadystatechange = function()
		{
			var nNumChildNodes = 0;
			if( 4 == mp_Request.readyState )
			{
				mp_Messages[ mp_Messages.length ] = "Response: state:" + mp_Request.readyState + " status:" + mp_Request.status + "";
				switch( mp_Request.status )
				{
				case 404:
					throw new AsynchRPC_Exception( "AsynchRPC::asynchfetchXML(): Bad server URL: '" + sServerURL + "'" );
					break;

				case 200:
					try
					{
						// have to locate the target before grabbing the XMLDocument,
						// otherwise IE changes the meaning of 'document' 
						oTargetElement = document.getElementById( sTargetElementID );
						if( !mp_Request.responseXML )
						{
							throw new AsynchRPC_ParseException( "AsynchRPC::asynchfetchXML(): responseXML is unavailable", true );
						}
						XMLDocument = mp_Request.responseXML.documentElement;
						if( null == XMLDocument )
						{
							throw new AsynchRPC_ParseException( "AsynchRPC::asynchfetchXML(): XMLDocument is null", true );
						}
						if( oTargetElement )
						{
							// empty target element
							nNumChildNodes = oTargetElement.childNodes.length; 
							for( var i=nNumChildNodes-1; i>=0; i--)
							{
								oTargetElement.removeChild( oTargetElement.childNodes.item(i) );
							}
							// then element-by-element transcribe the XML DOM into the target
							mp_Messages[ mp_Messages.length ] = "AsynchRPC::asynchfetchXML() attempting to write to '" + oTargetElement.id + "'";
							if( !pTranscribeXML( oTargetElement, XMLDocument ) )
							{
								// something went wrong with the transcription, so failover
								// to the text document
								throw new AsynchRPC_ParseException( "AsynchRPC::asynchfetchXML(): XML parse failed", true );
							}
						}
						else
						{
							throw new AsynchRPC_Exception( "AsynchRPC::asynchfetchXML(): Target Element id='" + sTargetElementID + "' is missing" );
						}
					}
					catch( exception )
					{
						if( exception.bSwitchToText )
						{
							// something happened, but attempt to recover by using 
							// the response text
							mp_Messages[ mp_Messages.length ] = exception.message;
							if( !pHandlePHPError( mp_Request.responseText ) )
							{
								pCleanAndStuffText( oTargetElement, mp_Request.responseText );
							}
						}
						else
						{
							mp_Messages[ mp_Messages.length ] = exception.message;
						}
					}
					// if the results have triggered debuging, view 
					// all the parsing messages and everything
					if( mp_bDebug )
					{
						self.AlertMessages();
					}
					break;
				};
			}
		};
		mp_Request.open( "GET", sServerURL, true );
		mp_Request.send( null );
	}
}

