/*
 * JSON/XML-RPC Client <http://code.google.com/p/json-xml-rpc/>
 * Version: 0.8.0.2 (2007-12-06)
 * Copyright: 2007, Weston Ruter <http://weston.ruter.net/>
 * License: GNU General Public License, Free Software Foundation
 *          <http://creativecommons.org/licenses/GPL/2.0/>
 *
 * Original inspiration for the design of this implementation is from jsolait, from which
 * are taken the "ServiceProxy" name and the interface for synchronous method calls.
 *
 * See the following specifications:
 *   - XML-RPC: <http://www.xmlrpc.com/spec>
 *   - JSON-RPC 1.0: <http://json-rpc.org/wiki/specification>
 *   - JSON-RPC 1.1 (draft): <http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html>
 *
 * Usage:
 * var service = new rpc.ServiceProxy("/app/service", {
 *                         asynchronous: true,   //default: true
 *                         sanitize: true,       //default: true
 *                         methods: ['greet'],   //default: null (synchronous introspection populates)
 *                         protocol: 'JSON-RPC', //default: JSON-RPC
 * });
 * service.greet({
 *    params:{name:"World"},
 *    onSuccess:function(message){
 *        alert(message);
 *    },
 *    onException:function(e){
 *        alert("Unable to greet because: " + e);
 *        return true;
 *    }
 * });
 *
 * If you create the service proxy with asynchronous set to false you may execute
 * the previous as follows:
 *
 * try {
 *    var message = service.greet("World");
 *    alert(message);
 * }
 * catch(e){
 *    alert("Unable to greet because: " + e);
 * }
 *
 * Finally, if the URL provided is on a site that violates the same origin policy,
 * then you may only create an asynchronous proxy, the resultant data may not be
 * sanitized, and you must provide the methods yourself. In order to obtain the
 * method response, the JSON-RPC server must be provided the name of a callback
 * function which will be generated in the JavaScript (json-in-script) response. The HTTP GET
 * parameter for passing the callback function is currently non-standardized and so
 * varies from server to server. Create a service proxy with the option
 * 'callbackParamName' in order to specify the callback function name parameter;
 * the default is 'JSON-response-callback', as used by associated JSON/XML-RPC
 * Server project. For example, getting Google Calendar data:
 *
 * var gcalService = new rpc.ServiceProxy("http://www.google.com/calendar/feeds/myemail%40gmail.com/public", {
 *                         asynchronous: true,  //true (default) required, otherwise error raised
 *                         sanitize: false,     //explicit false required, otherwise error raised
 *                         methods: ['full']    //explicit list required, otherwise error raised
 *                         callbackParamName: 'callback'
 *                         });
 * gcalService.full({
 *      params:{
 *          alt:'json-in-script' //required for this to work
 *          'start-min':new Date() //automatically converted to ISO8601
 *          //other Google Calendar parameters
 *      },
 *      onSuccess:function(json){
 *          json.feed.entry.each(function(entry){
 *              //do something
 *          });
 *      }
 * });
 */

var rpc = {
    version:"0.8.0.2",
    requestCount: 0
};

rpc.ServiceProxy = function(serviceURL, options){
    //if(typeof Prototype == 'undefined')
    //  throw Error("The RPC client currently requires the use of Prototype.");
    this.__serviceURL = serviceURL;

    //Determine if accessing the server would violate the same origin policy
    this.__isCrossSite = false;
    var urlParts = this.__serviceURL.match(/^(\w+:)\/\/([^\/]+?)(?::(\d+))?(?:$|\/)/);
    if(urlParts){
        this.__isCrossSite = (
            location.protocol !=  urlParts[1] ||
            document.domain   !=  urlParts[2] ||
            location.port     != (urlParts[3] || "")
        );
    }

    //Set other default options
    var providedMethodList;
    this.__isAsynchronous = true;
    this.__isResponseSanitized = true;
    this.__authUsername = null;
    this.__authPassword = null;
    this.__callbackParamName = 'JSON-response-callback';
    this.__protocol = 'JSON-RPC';
    this.__dateEncoding = 'ISO8601'; // ("@timestamp@" || "@ticks@") || "classHinting" || "ASP.NET"
    this.__decodeISO8601 = true; //JSON only

    //Get the provided options
    if(options instanceof Object){
        if(options.asynchronous !== undefined){
            this.__isAsynchronous = !!options.asynchronous;
            if(!this.__isAsynchronous && this.__isCrossSite)
                throw Error("It is not possible to establish a synchronous connection to a cross-site RPC service.");
        }
        if(options.sanitize != undefined)
            this.__isResponseSanitized = !!options.sanitize;
        if(options.user != undefined)
            this.__authUsername = options.user;
        if(options.password != undefined)
            this.__authPassword = options.password;
        if(options.callbackParamName != undefined)
            this.__callbackParamName = options.callbackParamName;
        if(String(options.protocol).toUpperCase() == 'XML-RPC')
            this.__protocol = 'XML-RPC';
        if(options.dateEncoding != undefined)
            this.__dateEncoding = options.dateEncoding;
        if(options.decodeISO8601 != undefined)
            this.__decodeISO8601 = !!options.decodeISO8601;
        providedMethodList = options.methods;
    }
    if(this.__isCrossSite){
        if(this.__isResponseSanitized){
            throw Error("You are attempting to access a service on another site, and the JSON data returned " +
                        "by cross-site requests cannot be sanitized. You must therefore explicitly set the " +
                        "'sanitize' option to false (it is true by default) in order to proceed with making " +
                        "potentially insecure cross-site rpc calls.");
        }
        else if(this.__protocol == 'XML-RPC')
            throw Error("Unable to use the XML-RPC protocol to access services on other domains.");
    }

    //Obtain the list of methods made available by the server
    if(this.__isCrossSite && !providedMethodList)
        throw Error("You must manually supply the service's method names since auto-introspection is not permitted for cross-site services.");
    if(providedMethodList)
        this.__methodList = providedMethodList;
    else {
        //Introspection must be performed synchronously
        var async = this.__isAsynchronous;
        this.__isAsynchronous = false;
        this.__methodList = this.__callMethod("system.listMethods", []);
        this.__isAsynchronous = async;
    }
    this.__methodList.push('system.listMethods');
    this.__methodList.push('system.describe');

    //Create local "wrapper" functions which reference the methods obtained above
    for(var methodName, i = 0; methodName = this.__methodList[i]; i++){
        //Make available the received methods in the form of chained property lists (eg. "parent.child.methodName")
        var methodObject = this;
        var propChain = methodName.split(/\./);
        for(var j = 0; j+1 < propChain.length; j++){
            if(!methodObject[propChain[j]])
                methodObject[propChain[j]] = {};
            methodObject = methodObject[propChain[j]];
        }

        //Create a wrapper to this.__callMethod with this instance and this methodName bound
        var wrapper = (function(instance, methodName){
            var call = {instance:instance, methodName:methodName}; //Pass parameters into closure
            return function(){
                if(call.instance.__isAsynchronous){
                    if(arguments.length == 1 && arguments[0] instanceof Object){
                        call.instance.__callMethod(call.methodName,
                                                 arguments[0].params,
                                                 arguments[0].onSuccess,
                                                 arguments[0].onException,
                                                 arguments[0].onComplete);
                    }
                    else {
                        call.instance.__callMethod(call.methodName,
                                                 arguments[0],
                                                 arguments[1],
                                                 arguments[2],
                                                 arguments[3]);
                    }
                    return undefined;
                }
                else return call.instance.__callMethod(call.methodName, rpc.toArray(arguments));
            };
        })(this, methodName);

        methodObject[propChain[propChain.length-1]] = wrapper;
    }
};

rpc.setAsynchronous = function(serviceProxy, isAsynchronous){
    if(!isAsynchronous && serviceProxy.__isCrossSite)
        throw Error("It is not possible to establish a synchronous connection to a cross-site RPC service.");
    serviceProxy.__isAsynchronous = !!isAsynchronous;
};

rpc.ServiceProxy.prototype.__callMethod = function(methodName, params, successHandler, exceptionHandler, completeHandler){
    rpc.requestCount++;

    //Verify that successHandler, exceptionHandler, and completeHandler are functions
    if(this.__isAsynchronous){
        if(successHandler && typeof successHandler != 'function')
            throw Error('The asynchronous onSuccess handler callback function you provided is invalid; the value you provided (' + successHandler.toString() + ') is of type "' + typeof(successHandler) + '".');
        if(exceptionHandler && typeof exceptionHandler != 'function')
            throw Error('The asynchronous onException handler callback function you provided is invalid; the value you provided (' + exceptionHandler.toString() + ') is of type "' + typeof(exceptionHandler) + '".');
        if(completeHandler && typeof completeHandler != 'function')
            throw Error('The asynchronous onComplete handler callback function you provided is invalid; the value you provided (' + completeHandler.toString() + ') is of type "' + typeof(completeHandler) + '".');
    }

    try {
        //Assign the provided callback function to the response lookup table
        if(this.__isAsynchronous || this.__isCrossSite){
            rpc.pendingRequests[String(rpc.requestCount)] = {
                //method:methodName,
                onSuccess:successHandler,
                onException:exceptionHandler,
                onComplete:completeHandler
            };
        }

        //Asynchronous cross-domain call (JSON-in-Script) -----------------------------------------------------
        if(this.__isCrossSite){ //then this.__isAsynchronous is implied

            //Create an ad hoc function specifically for this cross-site request; this is necessary because it is
            //  not possible pass an JSON-RPC request object with an id over HTTP Get requests.
            rpc.callbacks['r' + String(rpc.requestCount)] = (function(instance, id){
                var call = {instance: instance, id: id}; //Pass parameter into closure
                return function(response){
                    if(response instanceof Object && (response.result || response.error)){
                        response.id = call.id;
                        instance.__doCallback(response);
                    }
                    else {//Allow data without response wrapper (i.e. GData)
                        instance.__doCallback({id: call.id, result: response});
                    }
                }
            })(this, rpc.requestCount);
            //rpc.callbacks['r' + String(rpc.requestCount)] = new Function("response", 'response.id = ' + rpc.requestCount + '; this.__doCallback(response);');

            //Make the request by adding a SCRIPT element to the page
            var script = document.createElement('script');
            script.setAttribute('type', 'text/javascript');
            var src = this.__serviceURL +
                        '/' + methodName +
                        '?' + this.__callbackParamName + '=rpc.callbacks.r' + (rpc.requestCount);
            if(params)
                src += '&' + rpc.toQueryString(params);
            script.setAttribute('src', src);
            script.setAttribute('id', 'rpc' + rpc.requestCount);
            var head = document.getElementsByTagName('head')[0];
            rpc.pendingRequests[rpc.requestCount].scriptElement = script;
            head.appendChild(script);

            return undefined;
        }
        //Calls made with XMLHttpRequest ------------------------------------------------------------
        else {
            //Obtain and verify the parameters
            if(params){
                if(!(params instanceof Object) || params instanceof Date) //JSON-RPC 1.1 allows params to be a hash not just an array
                    throw Error('When making asynchronous calls, the parameters for the method must be passed as an array (or a hash); the value you supplied (' + String(params) + ') is of type "' + typeof(params) + '".');
                //request.params = params;
            }

            //Prepare the XML-RPC request
            var request,postData;
            if(this.__protocol == 'XML-RPC'){
                if(!(params instanceof Array))
                    throw Error("Unable to pass associative arrays to XML-RPC services.");

                var xml = ['<?xml version="1.0"?><methodCall><methodName>' + methodName + '</methodName>'];
                if(params){
                    xml.push('<params>');
                    for(var i = 0; i < params.length; i++)
                        xml.push('<param>' + this.__toXMLRPC(params[i]) + '</param>');
                    xml.push('</params>');
                }
                xml.push('</methodCall>');
                postData = xml.join('');

                //request = new Document();
                //var methodCallEl = document.createElement('methodCall');
                //var methodNameEl = document.createElement('methodName');
                //methodNameEl.appendChild(document.createTextNode(methodName));
                //methodCallEl.appendChild(methodNameEl);
                //if(params){
                //  var paramsEl = document.createElement('params');
                //  for(var i = 0; i < params.length; i++){
                //      var paramEl = document.createElement('param');
                //      paramEl.appendChild(this.__toXMLRPC(params[i]));
                //      paramsEl.appendChild(paramEl);
                //  }
                //  methodCallEl.appendChild(paramsEl);
                //}
                //request.appendChild(methodCallEl);
                //postData = request.serializeXML();
            }
            //Prepare the JSON-RPC request
            else {
                request = {
                    version:"1.1",
                    method:methodName,
                    id:rpc.requestCount
                };
                if(params)
                    request.params = params;
                postData = this.__toJSON(request);
            }

            //XMLHttpRequest chosen (over Ajax.Request) because it propogates uncaught exceptions
            var xhr;
            if(window.XMLHttpRequest)
                xhr = new XMLHttpRequest();
            else if(window.ActiveXObject){
                try {
                    xhr = new ActiveXObject('Msxml2.XMLHTTP');
                } catch(err){
                    xhr = new ActiveXObject('Microsoft.XMLHTTP');
                }
            }
            xhr.open('POST', this.__serviceURL, this.__isAsynchronous, this.__authUsername, this.__authPassword);
            if(this.__protocol == 'XML-RPC'){
                xhr.setRequestHeader('Content-Type', 'text/xml');
                xhr.setRequestHeader('Accept', 'text/xml');
            }
            else {
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.setRequestHeader('Accept', 'application/json');
            }

            //Asynchronous same-domain call -----------------------------------------------------
            if(this.__isAsynchronous){
                //Send the request
                xhr.send(postData);

                //Handle the response
                var instance = this;
                var requestInfo = {id:rpc.requestCount}; //for XML-RPC since the 'request' object cannot contain request ID
                xhr.onreadystatechange = function(){
                    //QUESTION: Why can't I use this.readyState?
                    if(xhr.readyState == 4){
                        //XML-RPC
                        if(instance.__protocol == 'XML-RPC'){
                            var response = instance.__getXMLRPCResponse(xhr, requestInfo.id);
                            instance.__doCallback(response);
                        }
                        //JSON-RPC
                        else {
                            var response = instance.__evalJSON(xhr.responseText, instance.__isResponseSanitized);
                            if(!response.id)
                                response.id = requestInfo.id;
                            instance.__doCallback(response);
                        }
                    }
                };

                return undefined;
            }
            //Synchronous same-domain call -----------------------------------------------------
            else {
                //Send the request
                xhr.send(postData);
                var response;
                if(this.__protocol == 'XML-RPC')
                    response = this.__getXMLRPCResponse(xhr, rpc.requestCount);
                else
                    response = this.__evalJSON(xhr.responseText, this.__isResponseSanitized);

                //Note that this error must be caught with a try/catch block instead of by passing a onException callback
                if(response.error)
                    throw Error('Unable to call "' + methodName + '". Server responsed with error (code ' + response.error.code + '): ' + response.error.message);

                this.__upgradeValuesFromJSON(response);
                return response.result;
            }
        }
    }
    catch(err){
        //err.locationCode = PRE-REQUEST Cleint
        var isCaught = false;
        if(exceptionHandler)
            isCaught = exceptionHandler(err); //add error location
        if(completeHandler)
            completeHandler();

        if(!isCaught)
            throw err;
    }
};

//This acts as a lookup table for the response callback to execute the user-defined
//   callbacks and to clean up after a request
rpc.pendingRequests = {};

//Ad hoc cross-site callback functions keyed by request ID; when a cross-site request
//   is made, a function is created
rpc.callbacks = {};

//Called by asychronous calls when their responses have loaded
rpc.ServiceProxy.prototype.__doCallback = function(response){
    if(typeof response != 'object')
        throw Error('The server did not respond with a response object.');
    if(!response.id)
        throw Error('The server did not respond with the required response id for asynchronous calls.');

    if(!rpc.pendingRequests[response.id])
        throw Error('Fatal error with RPC code: no ID "' + response.id + '" found in pendingRequests.');

    //Remove the SCRIPT element from the DOM tree for cross-site (JSON-in-Script) requests
    if(rpc.pendingRequests[response.id].scriptElement){
        var script = rpc.pendingRequests[response.id].scriptElement;
        script.parentNode.removeChild(script);
    }
    //Remove the ad hoc cross-site callback function
    if(rpc.callbacks[response.id])
        delete rpc.callbacks['r' + response.id];

    var uncaughtExceptions = [];

    //Handle errors returned by the server
    if(response.error !== undefined){
        var err = new Error(response.error.message);
        err.code = response.error.code;
        //err.locationCode = SERVER
        if(rpc.pendingRequests[response.id].onException){
            try{
                if(!rpc.pendingRequests[response.id].onException(err))
                    uncaughtExceptions.push(err);
            }
            catch(err2){ //If the onException handler also fails
                uncaughtExceptions.push(err);
                uncaughtExceptions.push(err2);
            }
        }
        else uncaughtExceptions.push(err);
    }

    //Process the valid result
    else if(response.result !== undefined){
        //iterate over all values and substitute date strings with Date objects
        //Note that response.result is not passed because the values contained
        //  need to be modified by reference, and the only way to do so is
        //  but accessing an object's properties. Thus an extra level of
        //  abstraction allows for accessing all of the results members by reference.
        this.__upgradeValuesFromJSON(response);

        if(rpc.pendingRequests[response.id].onSuccess){
            try {
                rpc.pendingRequests[response.id].onSuccess(response.result);
            }
            //If the onSuccess callback itself fails, then call the onException handler as above
            catch(err){
                //err3.locationCode = CLIENT;
                if(rpc.pendingRequests[response.id].onException){
                    try {
                        if(!rpc.pendingRequests[response.id].onException(err))
                            uncaughtExceptions.push(err);
                    }
                    catch(err2){ //If the onException handler also fails
                        uncaughtExceptions.push(err);
                        uncaughtExceptions.push(err2);
                    }
                }
                else uncaughtExceptions.push(err);
            }
        }
    }

    //Call the onComplete handler
    try {
        if(rpc.pendingRequests[response.id].onComplete)
            rpc.pendingRequests[response.id].onComplete(response);
    }
    catch(err){ //If the onComplete handler fails
        //err3.locationCode = CLIENT;
        if(rpc.pendingRequests[response.id].onException){
            try {
                if(!rpc.pendingRequests[response.id].onException(err))
                    uncaughtExceptions.push(err);
            }
            catch(err2){ //If the onException handler also fails
                uncaughtExceptions.push(err);
                uncaughtExceptions.push(err2);
            }
        }
        else uncaughtExceptions.push(err);
    }

    delete rpc.pendingRequests[response.id];

    //Merge any exception raised by onComplete into the previous one(s) and throw it
    if(uncaughtExceptions.length){
        var code;
        var message = 'There ' + (uncaughtExceptions.length == 1 ?
                             'was 1 uncaught exception' :
                             'were ' + uncaughtExceptions.length + ' uncaught exceptions') + ': ';
        for(var i = 0; i < uncaughtExceptions.length; i++){
            if(i)
                message += "; ";
            message += uncaughtExceptions[i].message;
            if(uncaughtExceptions[i].code)
                code = uncaughtExceptions[i].code;
        }
        var err = new Error(message);
        err.code = code;
        throw err;
    }
};


/*******************************************************************************************
 * JSON-RPC Specific Functions
 ******************************************************************************************/
rpc.ServiceProxy.prototype.__toJSON = function(value){
    switch(typeof value){
        case 'number':
            return isFinite(value) ? value.toString() : 'null';
        case 'boolean':
            return value.toString();
        case 'string':
            //Taken from Ext JSON.js
            var specialChars = {
                "\b": '\\b',
                "\t": '\\t',
                "\n": '\\n',
                "\f": '\\f',
                "\r": '\\r',
                '"' : '\\"',
                "\\": '\\\\',
                "/" : '\/'
            };
            return '"' + value.replace(/([\x00-\x1f\\"])/g, function(a, b) {
                var c = specialChars[b];
                if(c)
                    return c;
                c = b.charCodeAt();
                //return "\\u00" + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
                return '\\u00' + rpc.zeroPad(c.toString(16));
            }) + '"';
        case 'object':
            if(value === null)
                return 'null';
            else if(value instanceof Array){
                var json = ['['];  //Ext's JSON.js reminds me that Array.join is faster than += in MSIE
                for(var i = 0; i < value.length; i++){
                    if(i)
                        json.push(',');
                    json.push(this.__toJSON(value[i]));
                }
                json.push(']');
                return json.join('');
            }
            else if(value instanceof Date){
                switch(this.__dateEncoding){
                    case 'classHinting': //{"__jsonclass__":["constructor", [param1,...]], "prop1": ...}
                        return '{"__jsonclass__":["Date",[' + value.valueOf() + ']]}';
                    case '@timestamp@':
                    case '@ticks@':
                        return '"@' + value.valueOf() + '@"';
                    case 'ASP.NET':
                        return '"\\/Date(' + value.valueOf() + ')\\/"';
                    default:
                        return '"' + rpc.dateToISO8601(value) + '"';
                }
            }
            else if(value instanceof Number || value instanceof String || value instanceof Boolean)
                return this.__toJSON(value.valueOf());
            else {
                var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js
                var json = ['{'];
                for(var key in value){
                    if(!useHasOwn || value.hasOwnProperty(key)){
                        if(json.length > 1)
                            json.push(',');
                        json.push(this.__toJSON(key) + ':' + this.__toJSON(value[key]));
                    }
                }
                json.push('}');
                return json.join('');
            }
        //case 'undefined':
        //case 'function':
        //case 'unknown':
        //default:
    }
    throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to JSON.'); //(' + String(value) + ')
};

rpc.isJSON = function(string){ //from Prototype String.isJSON()
    var testStr = string.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(testStr);
};

rpc.ServiceProxy.prototype.__evalJSON = function(json, sanitize){ //from Prototype String.evalJSON()
    //Remove security comment delimiters
    json = json.replace(/^\/\*-secure-([\s\S]*)\*\/\s*$/, "$1");
    var err;
    try {
        if(!sanitize || rpc.isJSON(json))
            return eval('(' + json + ')');
    }
    catch(e){err = e;}
    throw new SyntaxError('Badly formed JSON string: ' + json + " ... " + (err ? err.message : ''));
};

//This function iterates over the properties of the passed object and converts them
//   into more appropriate data types, i.e. ISO8601 strings are converted to Date objects.
rpc.ServiceProxy.prototype.__upgradeValuesFromJSON = function(obj){
    var matches, useHasOwn = {}.hasOwnProperty ? true : false;
    for(var key in obj){
        if(!useHasOwn || obj.hasOwnProperty(key)){
            //Parse date strings
            if(typeof obj[key] == 'string'){
                //ISO8601
                if(this.__decodeISO8601 && (matches = obj[key].match(/^(?:(\d\d\d\d)-(\d\d)(?:-(\d\d)(?:T(\d\d)(?::(\d\d)(?::(\d\d)(?:\.(\d+))?)?)?)?)?)$/))){
                    obj[key] = new Date(0);
                    if(matches[1]) obj[key].setUTCFullYear(parseInt(matches[1]));
                    if(matches[2]) obj[key].setUTCMonth(parseInt(matches[2]-1));
                    if(matches[3]) obj[key].setUTCDate(parseInt(matches[3]));
                    if(matches[4]) obj[key].setUTCHours(parseInt(matches[4]));
                    if(matches[5]) obj[key].setUTCMinutes(parseInt(matches[5]));
                    if(matches[6]) obj[key].setUTCMilliseconds(parseInt(matches[6]));
                }
                //@timestamp@ / @ticks@
                else if(matches = obj[key].match(/^@(\d+)@$/)){
                    obj[key] = new Date(parseInt(matches[1]))
                }
                //ASP.NET
                else if(matches = obj[key].match(/^\/Date\((\d+)\)\/$/)){
                    obj[key] = new Date(parseInt(matches[1]))
                }
            }
            else if(obj[key] instanceof Object){

                //JSON 1.0 Class Hinting: {"__jsonclass__":["constructor", [param1,...]], "prop1": ...}
                if(obj[key].__jsonclass__ instanceof Array){
                    //console.info('good1');
                    if(obj[key].__jsonclass__[0] == 'Date'){
                        //console.info('good2');
                        if(obj[key].__jsonclass__[1] instanceof Array && obj[key].__jsonclass__[1][0])
                            obj[key] = new Date(obj[key].__jsonclass__[1][0]);
                        else
                            obj[key] = new Date();
                    }
                }
                else this.__upgradeValuesFromJSON(obj[key]);
            }
        }
    }
};


/*******************************************************************************************
 * XML-RPC Specific Functions
 ******************************************************************************************/

rpc.ServiceProxy.prototype.__toXMLRPC = function(value){
    var xml = ['<value>'];
    switch(typeof value){
        case 'number':
            if(!isFinite(value))
                xml.push('<nil/>');
            else if(parseInt(value) == Math.ceil(value)){
                xml.push('<int>');
                xml.push(value.toString());
                xml.push('</int>');
            }
            else {
                xml.push('<double>');
                xml.push(value.toString());
                xml.push('</double>');
            }
            break;
        case 'boolean':
            xml.push('<boolean>');
            xml.push(value ? '1' : '0');
            xml.push('</boolean>');
            break;
        case 'string':
            xml.push('<string>');
            xml.push(value.replace(/[<>&]/, function(ch){

            })); //escape for XML!
            xml.push('</string>');
            break;
        case 'object':
            if(value === null)
                xml.push('<nil/>');
            else if(value instanceof Array){
                xml.push('<array><data>');
                for(var i = 0; i < value.length; i++)
                    xml.push(this.__toXMLRPC(value[i]));
                xml.push('</data></array>');
            }
            else if(value instanceof Date){
                xml.push('<dateTime.iso8601>' + rpc.dateToISO8601(value) + '</dateTime.iso8601>');
            }
            else if(value instanceof Number || value instanceof String || value instanceof Boolean)
                return rpc.dateToISO8601(value.valueOf());
            else {
                xml.push('<struct>');
                var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js
                for(var key in value){
                    if(!useHasOwn || value.hasOwnProperty(key)){
                        xml.push('<member>');
                        xml.push('<name>' + key + '</name>'); //Excape XML!
                        xml.push(this.__toXMLRPC(value[key]));
                        xml.push('</member>');
                    }
                }
                xml.push('</struct>');
            }
            break;
        //case 'undefined':
        //case 'function':
        //case 'unknown':
        default:
            throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to XML-RPC.'); //(' + String(value) + ')
    }
    xml.push('</value>');
    return xml.join('');
};

//rpc.ServiceProxy.prototype.toXMLRPC = function(value){ //documentNode
//  var valueEl = document.createElement('value');
//  //var xml = ['<value>'];
//  switch(typeof value){
//      case 'number':
//          if(!isFinite(value))
//              //xml.push('<nil/>');
//              valueEl.appendChild(document.createElement('nil'));
//          //else if(parseInt(value) == Math.ceil(value)){
//          //  var intEl = document.createElement('int');
//          //  intEl.appendChild(document.createTextNode(value.toString()));
//          //  valueEl.appendChild(intEl);
//          //  //xml.push('<int>');
//          //  //xml.push(value.toString());
//          //  //xml.push('</int>');
//          //}
//          //else {
//          //  var doubleEl = document.createElement('double');
//          //  doubleEl.appendChild(document.createTextNode(value.toString()));
//          //  valueEl.appendChild(doubleEl);
//          //  //xml.push('<double>');
//          //  //xml.push(value.toString());
//          //  //xml.push('</double>');
//          //}
//          else {
//              var numEl = document.createElement(parseInt(value) == Math.ceil(value) ? 'int' : 'double');
//              numEl.appendChild(document.createTextNode(value.toString()));
//              valueEl.appendChild(numEl);
//          }
//          return valueEl;
//      case 'boolean':
//          var boolEl = document.createElement('boolean');
//          boolEl.appendChild(document.createTextNode(value ? '1' : '0'));
//          valueEl.appendChild(boolEl);
//          return valueEl;
//          //xml.push('<boolean>');
//          //xml.push(value ? '1' : '0');
//          //xml.push('</boolean>');
//      case 'string':
//          var stringEl = document.createElement('string');
//          stringEl.appendChild(document.createTextNode(value));
//          valueEl.appendChild(stringEl);
//          return valueEl;
//      case 'object':
//          if(value === null)
//              valueEl.appendChild(document.createElement('nil'));
//          else if(value instanceof Array){
//              var arrayEl = document.createElement('array');
//              var dataEl = document.createElement('data');
//              for(var i = 0; i < value.length; i++)
//                  dataEl.appendChild(this.__toXMLRPC(value[i]));
//              arrayEl.appendChild(dataEl);
//              valueEl.appendChild(arrayEl);
//          }
//          else if(value instanceof Date){
//              var dateEl = document.createElement('datetime.ISO8601');
//              dateEl.appendChild(document.createTextNode(rpc.dateToISO8601(value)));
//              valueEl.appendChild(dateEl);
//          }
//          else if(value instanceof Number || value instanceof String || value instanceof Boolean)
//              return rpc.dateToISO8601(value.valueOf());
//          else {
//              var structEl = document.createElement('struct');
//              var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js
//              for(var key in value){
//                  if(!useHasOwn || value.hasOwnProperty(key)){
//                      var memberEl = document.createElement('member');
//                      var nameEl = document.createElement('name')
//                      nameEl.appendChild(document.createTextNode(key));
//                      memberEl.appendChild(nameEl);
//                      memberEl.appendChild(this.__toXMLRPC(value[key]));
//                      structEl.appendChild(memberEl);
//                  }
//              }
//              valueEl.appendChild(structEl);
//          }
//          return valueEl;
//      //case 'undefined':
//      //case 'function':
//      //case 'unknown':
//      //default:
//  }
//  throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to XML-RPC.'); //(' + String(value) + ')
//};

rpc.ServiceProxy.prototype.__parseXMLRPC = function(valueEl){
    if(valueEl.childNodes.length == 1 &&
       valueEl.childNodes.item(0).nodeType == 3)
    {
        return valueEl.childNodes.item(0).nodeValue;
    }
    for(var i = 0; i < valueEl.childNodes.length; i++){
        if(valueEl.childNodes.item(i).nodeType == 1){
            var typeEL = valueEl.childNodes.item(i);
            switch(typeEL.nodeName.toLowerCase()){
                case 'i4':
                case 'int':
                    //An integer is a 32-bit signed number. You can include a plus or minus at the
                    //   beginning of a string of numeric characters. Leading zeros are collapsed.
                    //   Whitespace is not permitted. Just numeric characters preceeded by a plus or minus.
                    var intVal = parseInt(typeEL.firstChild.nodeValue);
                    if(isNaN(intVal))
                        throw Error("XML-RPC Parse Error: The value provided as an integer '" + typeEL.firstChild.nodeValue + "' is invalid.");
                    return intVal;
                case 'double':
                    //There is no representation for infinity or negative infinity or "not a number".
                    //   At this time, only decimal point notation is allowed, a plus or a minus,
                    //   followed by any number of numeric characters, followed by a period and any
                    //   number of numeric characters. Whitespace is not allowed. The range of
                    //   allowable values is implementation-dependent, is not specified.
                    var floatVal = parseFloat(typeEL.firstChild.nodeValue);
                    if(isNaN(floatVal))
                        throw Error("XML-RPC Parse Error: The value provided as a double '" + typeEL.firstChild.nodeValue + "' is invalid.");
                    return floatVal;
                case 'boolean':
                    if(typeEL.firstChild.nodeValue != '0' && typeEL.firstChild.nodeValue != '1')
                        throw Error("XML-RPC Parse Error: The value provided as a boolean '" + typeEL.firstChild.nodeValue + "' is invalid.");
                    return Boolean(parseInt(typeEL.firstChild.nodeValue));
                case 'string':
                    if(!typeEL.firstChild)
                        return "";
                    return typeEL.firstChild.nodeValue;
                case 'datetime.iso8601':
                    var matches, date = new Date(0);
                    if(matches = typeEL.firstChild.nodeValue.match(/^(?:(\d\d\d\d)-(\d\d)(?:-(\d\d)(?:T(\d\d)(?::(\d\d)(?::(\d\d)(?:\.(\d+))?)?)?)?)?)$/)){
                        if(matches[1]) date.setUTCFullYear(parseInt(matches[1]));
                        if(matches[2]) date.setUTCMonth(parseInt(matches[2]-1));
                        if(matches[3]) date.setUTCDate(parseInt(matches[3]));
                        if(matches[4]) date.setUTCHours(parseInt(matches[4]));
                        if(matches[5]) date.setUTCMinutes(parseInt(matches[5]));
                        if(matches[6]) date.setUTCMilliseconds(parseInt(matches[6]));
                        return date;
                    }
                    throw Error("XML-RPC Parse Error: The provided value does not match ISO8601.");
                case 'base64':
                    throw Error("Not able to parse base64 data yet.");
                    //return base64_decode(typeEL.firstChild.nodeValue);
                case 'nil':
                    return null;
                case 'struct':
                    //A <struct> contains <member>s and each <member> contains a <name> and a <value>.
                    var obj = {};
                    for(var memberEl, j = 0; memberEl = typeEL.childNodes.item(j); j++){
                        if(memberEl.nodeType == 1 && memberEl.nodeName == 'member'){
                            var name = '';
                            valueEl = null;
                            for(var child, k = 0; child = memberEl.childNodes.item(k); k++){
                                if(child.nodeType == 1){
                                    if(child.nodeName == 'name')
                                        name = child.firstChild.nodeValue;
                                    else if(child.nodeName == 'value')
                                        valueEl = child;
                                }
                            }
                            //<struct>s can be recursive, any <value> may contain a <struct> or
                            //   any other type, including an <array>, described below.
                            if(name && valueEl)
                                obj[name] = this.__parseXMLRPC(valueEl);
                        }
                    }
                    return obj;
                case 'array':
                    //An <array> contains a single <data> element, which can contain any number of <value>s.
                    var arr = [];
                    var dataEl = typeEL.firstChild;
                    while(dataEl && (dataEl.nodeType != 1 || dataEl.nodeName != 'data'))
                        dataEl = dataEl.nextSibling;

                    if(!dataEl)
                        new Error("XML-RPC Parse Error: Expected 'data' element as sole child element of 'array'.");

                    valueEl = dataEl.firstChild;
                    while(valueEl){
                        if(valueEl.nodeType == 1){
                            //<arrays>s can be recursive, any value may contain an <array> or
                            //   any other type, including a <struct>, described above.
                            if(valueEl.nodeName == 'value')
                                arr.push(this.__parseXMLRPC(valueEl));
                            else
                                throw Error("XML-RPC Parse Error: Illegal element child '" + valueEl.nodeName + "' of an array's 'data' element.");
                        }
                        valueEl = valueEl.nextSibling;
                    }
                    return arr;
                default:
                    throw Error("XML-RPC Parse Error: Illegal element '" + typeEL.nodeName + "' child of the 'value' element.");
            }
        }
    }
    return '';
}

rpc.ServiceProxy.prototype.__getXMLRPCResponse = function(xhr, id){
    var response = {};
    if(!xhr.responseXML)
        throw Error("Malformed XML document.");
    var doc = xhr.responseXML.documentElement;
    if(doc.nodeName != 'methodResponse')
        throw Error("Invalid XML-RPC document.");

    var valueEl = doc.getElementsByTagName('value')[0];
    if(valueEl.parentNode.nodeName == 'param' &&
       valueEl.parentNode.parentNode.nodeName == 'params')
    {
        response.result = this.__parseXMLRPC(valueEl);
    }
    else if(valueEl.parentNode.nodeName == 'fault'){
        var fault = this.__parseXMLRPC(valueEl);
        response.error = {
            code: fault.faultCode,
            message: fault.faultString
        };
    }
    else throw Error("Invalid XML-RPC document.");

    if(!response.result && !response.error)
        throw Error("Malformed XML-RPC methodResponse document.");

    response.id = id; //XML-RPC cannot pass and return request IDs
    return response;
};

/*******************************************************************************************
 * Other helper functions
 ******************************************************************************************/

//Takes an array or hash and coverts it into a query string, converting dates to ISO8601
//   and throwing an exception if nested hashes or nested arrays appear.
rpc.toQueryString = function(params){
    if(!(params instanceof Object || params instanceof Array) || params instanceof Date)
        throw Error('You must supply either an array or object type to convert into a query string. You supplied: ' + params.constructor);

    var str = '';
    var useHasOwn = {}.hasOwnProperty ? true : false;

    for(var key in params){
        if(useHasOwn && params.hasOwnProperty(key)){
            //Process an array
            if(params[key] instanceof Array){
                for(var i = 0; i < params[key].length; i++){
                    if(str)
                        str += '&';
                    str += encodeURIComponent(key) + "=";
                    if(params[key][i] instanceof Date)
                        str += encodeURIComponent(rpc.dateToISO8601(params[key][i]));
                    else if(params[key][i] instanceof Object)
                        throw Error('Unable to pass nested arrays nor objects as parameters while in making a cross-site request. The object in question has this constructor: ' + params[key][i].constructor);
                    else str += encodeURIComponent(String(params[key][i]));
                }
            }
            else {
                if(str)
                    str += '&';
                str += encodeURIComponent(key) + "=";
                if(params[key] instanceof Date)
                    str += encodeURIComponent(rpc.dateToISO8601(params[key]));
                else if(params[key] instanceof Object)
                    throw Error('Unable to pass objects as parameters while in making a cross-site request. The object in question has this constructor: ' + params[key].constructor);
                else str += encodeURIComponent(String(params[key]));
            }
        }
    }
    return str;
};

//Converts an iterateable value into an array; similar to Prototype's $A function
rpc.toArray = function(value){
    //if(value && value.length){
        if(value instanceof Array)
            return value;
        var array = [];
        for(var i = 0; i < value.length; i++)
            array.push(value[i]);
        return array;
    //}
    //throw Error("Unable to convert to an array the value: " + String(value));
};

//Returns an ISO8601 string *in UTC* for the provided date (Prototype's Date.toJSON() returns localtime)
rpc.dateToISO8601 = function(date){
    //var jsonDate = date.toJSON();
    //return jsonDate.substring(1, jsonDate.length-1); //strip double quotes

    return date.getUTCFullYear()             + '-' +
           rpc.zeroPad(date.getUTCMonth()+1) + '-' +
           rpc.zeroPad(date.getUTCDate())    + 'T' +
           rpc.zeroPad(date.getUTCHours())   + ':' +
           rpc.zeroPad(date.getUTCMinutes()) + ':' +
           rpc.zeroPad(date.getUTCSeconds()) + '.' +
           //Prototype's Date.toJSON() method does not include milliseconds
           rpc.zeroPad(date.getUTCMilliseconds(), 3);
};

rpc.zeroPad = function(value, width){
    if(!width)
        width = 2;
    value = (value == undefined ? '' : String(value))
    while(value.length < width)
        value = '0' + value;
    return value;
};