var xmlRPCRequests = new Array ( );
var nextXMLRPCRequestId = 1;

function xmlRPCRequest ( url, method, requestHandler )
{
	this.url			= url;
	this.method			= method;
	this.requestHandler	= requestHandler;
	
	this.arguments		= null;
	
	if ( arguments.length > 3 )
	{
		this.arguments = new Array ( );
		
		for ( var index = 3; index < arguments.length; index++ )
		{
			this.arguments.push ( arguments [ index ] );
		}
	}
	
	this.id				= nextXMLRPCRequestId++;
	this.parameters		= null;
	this.request		= null;
	this.lastReadyState	= null;
	this.lastStatus		= null;
	this.isFault		= false;
	this.response		= null;
	this.timeout		= null;
	this.timerId		= null;
	this.timedOut		= false;
	
	xmlRPCRequests.push ( this );
	
	return this;
}

xmlRPCRequest.prototype.setTimeout = function ( timeout )
{
	this.timeout = timeout;
}

xmlRPCRequest.prototype.formatValue = function ( type, value )
{
	return '<value><' + type + '>' + ( value == null ? '' : value ) + '</' + type + '></value>';
}

xmlRPCRequest.prototype.parseValue = function ( value )
{
	var parsedValue;
	
	if ( typeof ( value ) == "number" )
	{
		parsedValue = this.formatValue ( "int", value );
	}
	else if ( typeof ( value ) == "string" )
	{
		if ( ! value.match ( /^<value>.*<\/value>$/ ) )
		{
			parsedValue = this.formatValue ( "string", "<![CDATA[" + value + "]]>" );
		}
		else
		{
			parsedValue = value;
		}
	}
	else if ( typeof ( value ) == "boolean" )
	{
		parsedValue = this.formatValue ( "boolean", value );
	}
	else if ( typeof ( value ) == "object" )
	{
		if ( value.push != null && typeof ( value.push ) == "function" )
		{
			// An array.
			
			var parsedValues = '';
			
			for ( var index = 0; index < value.length; index++ )
			{
				parsedValues += '<value>' + this.parseValue ( value [ index ] ) + '</value>';
			}
			
			parsedValue = '<array><data>' + parsedValues + '</data></array>';
		}
		else
		{
			// A struct.
			
			var parsedValues = '';
			
			for ( var key in value )
			{
				parsedValues += '<member><name>' + key + '</name><value>' + this.parseValue ( value [ key ] ) + '</value></member>';
			}
			
			parsedValue = '<struct>' + parsedValues + '</struct>';
		}
	}
	else
	{
		throw "illegal value for XML request";
	}
	
	return parsedValue;
}

xmlRPCRequest.prototype.addParameter = function ( value )
{
	if ( this.parameters == null )
	{
		this.parameters = new Array ( );
	}
	
	this.parameters.push ( this.parseValue ( value ) );
}

xmlRPCRequest.prototype.marshalRequest = function ( )
{
	var xmlRequest = '<?xml version="1.0"?><methodCall><methodName>' + this.method + '</methodName>';
	
	if ( this.parameters != null && this.parameters.length > 0 )
	{
		xmlRequest += '<params>';

		for ( var index = 0; index < this.parameters.length; index++ )
		{
			xmlRequest += '<param>' + this.parameters [ index ]+ '</param>';
		}
		
		xmlRequest += '</params>';
	}
   
	xmlRequest += '</methodCall>';

	return xmlRequest;
}

xmlRPCRequest.prototype.send = function ( )
{
	this.request	= null;
	this.isFault	= false;
	this.response	= null;
	
	if ( this.timerId != null )
	{
		clearTimeout ( this.timerId );
		this.timerId = null;
		this.timedOut = false;
	}
	
    if ( window.XMLHttpRequest )
	{
    	try 
		{
			this.request = new XMLHttpRequest ( );
        }
		catch ( e )
		{
			this.request = null;
        }
    } 
	else if ( window.ActiveXObject ) 
	{
       	try
		{
        	this.request = new ActiveXObject ( "Msxml2.XMLHTTP" );
      	}
		catch ( e )
		{
        	try
			{
          		this.request = new ActiveXObject ( "Microsoft.XMLHTTP" );
        	}
			catch ( e )
			{
          		this.request = null;
        	}
		}
    }
	
	if ( this.request != null )
	{
		this.request.onreadystatechange = this.onRequestChange;
		this.request.open ( "POST", this.url, true );
		this.request.setRequestHeader ( 'Content-Type', 'text/xml' );
		this.request.send ( this.marshalRequest ( ) );
		
		if ( this.timeout != null )
		{
			this.timerId = setTimeout ( "onXMLRPCRequestTimeout ( " + this.id + " )", this.timeout );
		}
	}
	else
	{
		throw "unable to create a new XML request";
	}
}

function onXMLRPCRequestTimeout ( requestId )
{
	for ( var index = 0; index < xmlRPCRequests.length; index++ )
	{
		var req = xmlRPCRequests [ index ];
		
		if ( req.id == requestId )
		{
			req.request.abort ( );
			req.lastStatus = 500;
			this.timedOut = true;
			
			if ( req.arguments != null )
			{
				req.requestHandler.apply ( req, req.arguments );
			}
			else
			{
				req.requestHandler.apply ( req );
			}
			
			break;
		}
	}
}

xmlRPCRequest.prototype.onRequestChange = function ( )
{
	// scan the requests
	
	for ( var index = 0; index < xmlRPCRequests.length; index++ )
	{
		var req = xmlRPCRequests [ index ];
		
		// has this request changed? we are super careful to check because these
		// members don't seem to be very robustly set at least in firefox.
		
		var changed = false;
		
		try
		{
			changed = req.request != null && ( req.request.readyState != req.lastReadyState || ( req.request.readyState == 4 && req.request.status != req.lastStatus ) );
		}
		catch ( e )
		{
		}
		
		if ( changed )
		{
			req.lastReadyState = req.request.readyState;
			
			if ( req.request.readyState == 4 )
			{
				var status;
				
				try
				{
					// This throws exceptions on occasion - indications are this
					// is a browser bug.
					
					status = req.request.status;
				}
				catch ( e )
				{
					status = 500;
				}
					
				req.lastStatus = status;
				
				if ( status == 200 )
				{
					req.parseXMLResponse ( );
				}
				
				if ( this.timerId != null )
				{
					clearTimeout ( this.timerId );
					this.timerId = null;
				}
			}
			
			if ( req.arguments != null )
			{
				req.requestHandler.apply ( req, req.arguments );
			}
			else
			{
				req.requestHandler.apply ( req );
			}
		}
	}
}

xmlRPCRequest.prototype.parseXMLResponse = function ( )
{
	var valueNode;
	
	if ( this.request.responseXML.documentElement != null )
	{
		valueNode = this.request.responseXML.documentElement.firstChild;
	}
	else
	{
		valueNode = this.request.responseXML.getElementsByTagName ( "response" ) [ 0 ].firstChild;
	}
	
	while ( valueNode != null && ! ( valueNode.nodeName == "param" || valueNode.nodeName == "fault" ) )
	{
		valueNode = valueNode.firstChild;
	}

	this.isFault = valueNode.nodeName == "fault" ? true : false;
	
	this.response = this.parseValueNode ( valueNode.firstChild );
}

xmlRPCRequest.prototype.parseValueNode = function ( valueNode )
{
	var value = null;
	
	var firstChild     = valueNode.firstChild;
	var firstChildName = valueNode.firstChild.nodeName;
	
	if ( firstChildName == "struct" )
	{
		value = new Object ( );
		
		var members = firstChild.childNodes;
		
		for ( var index = 0; index < members.length; index++ )
		{
			var entries = members [ index ].childNodes;
			
			var entryName;
			var entryValue;
			
			for ( var index2 = 0; index2 < entries.length; index2++ )
			{
				if ( entries [ index2 ].nodeName == "name" )
				{
					entryName = entries [ index2 ].firstChild.nodeValue;
				}
				else
				{
					entryValue = this.parseValueNode ( entries [ index2 ] );
				}
			}
			
			value [ entryName ] = entryValue;
		}
	}
	else if ( firstChildName == "array" )
	{
		value = new Array ( );
		
		var values = firstChild.firstChild.childNodes;
		
		for ( var index = 0; index < values.length; index++ )
		{
			value.push ( this.parseValueNode ( values [ index ] ) );
		}
	}
	else
	{
		if ( firstChild.firstChild != null )
		{
			value = firstChild.firstChild.nodeValue;
		}
	}
	
	return value;
}

xmlRPCRequest.prototype.complete = function ( )
{
	for ( var index = 0; index < xmlRPCRequests.length; index++ )
	{
		if ( this == xmlRPCRequests [ index ] )
		{
			xmlRPCRequests.splice ( index, 1 );
			break;
		}
	}
}

function onRequestErrorCheck ( requestor )
{
	if ( requestor.timedOut )
	{
		throw ( 'Please try again, communication with the server timed out.' );
	}
	
	if ( requestor.request.readyState == 4 && requestor.lastStatus != 200 )
	{
		throw ( 'Please try again, communication with the server failed with code ' + requestor.lastStatus + '.' );
	}
	
	if ( requestor.isFault )
	{
		throw ( requestor.response.faultString.replace ( /^ExceptionClass:[^ ]* /g, '' ) );
	}
}
