// vim: ts=4 sw=4 expandtab cindent foldmethod=marker
//
// BEGIN FLOCK GPL
// 
// Copyright Flock Inc. 2005-2006
// http://flock.com
// 
// This file may be used under the terms of of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
// 
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
// 
// END FLOCK GPL
//

const nsISupports = Components.interfaces.nsISupports;
const nsITimer = Components.interfaces.nsITimer;
const nsITimerCallback = Components.interfaces.nsITimerCallback;
const nsXMLHttpRequest = Components.interfaces.nsXMLHttpRequest;
const TIMER_CONTRACTID = '@mozilla.org/timer;1';
const XMLHTTPREQUEST_CONTRACTID = '@mozilla.org/xmlextras/xmlhttprequest;1';


function DEBUG(X) { }
//function DEBUG(X) { debug ('flockXmlRpc.js: '+X+'\n'); }


const Base64 = {
    chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
    encode: function (aInput) {
        var output = '';
        while (aInput.length > 0) {
            output += Base64.chars[aInput.charCodeAt(0) >> 2];
            output += Base64.chars[((aInput.charCodeAt(0)&0x03) << 4) | 
                (aInput.length>1?((aInput.charCodeAt(1)&0xF0) >> 4):0)];
            output += Base64.chars[aInput.length>1?
                ((aInput.charCodeAt(1)&0x0F)<<2) | 
                (aInput.length>2?((aInput.charCodeAt(2)&0xC0) >> 6):0):64];
            output += Base64.chars[aInput.length>2?
                (aInput.charCodeAt(2)&0x3F):64];
            if (aInput.length > 3) {
                aInput = aInput.substr (3);
            } else {
                break;
            }
        }
        return output;
    },
    decode: function (aInput) {
        const regexp = new RegExp ('[^'+Base64.chars+']', 'g');
        var output = '';

        aInput = aInput.replace (regexp, '');

        // FIXME: aInput MUST now be a multiple of four characters long

        function sixbits (i) {
            if (i == '=') return 0;
            return Base64.chars.indexOf (i);
        }

        while (aInput.length >= 4) {
            output += String.fromCharCode ((sixbits (aInput[0]) << 2) | 
                    (sixbits (aInput[1]) >> 4));
            if (aInput[2] == '=') {
                break;
            }
            output += String.fromCharCode ((sixbits (aInput[1]) << 4) |
                    (sixbits (aInput[2]) >>2));
            if (aInput[3] == '=') {
                break;
            }
            output += String.fromCharCode (((sixbits (aInput[2]) << 6) & 0xC0) |
                    sixbits (aInput[3]));
            aInput = aInput.substr (4);
        }
        return output;
    },
}




XmlRpcXml = {
    build: function (value) {
        if (value == undefined) {
            return null;
        }

        // FIXME: do the next two blocks have the desired effect?
        if (value.QueryInterface) {
            value = value.QueryInterface (flockIXmlRpcValue);
            if (value == null) {
                return null;
            }
        } else {
            return null;
        }
        try {
            value = value.QueryInterface (flockIXmlRpcValue);
        } catch (e) {
            return value.toString ();
        }

        switch (value.XmlRpcType) {
            case flockIXmlRpcValue.TYPE_INT:
                return <int>{String(value.IntValue)}</int>;
            case flockIXmlRpcValue.TYPE_BOOLEAN:
                return <boolean>{value.BooleanValue?1:0}</boolean>;
            case flockIXmlRpcValue.TYPE_STRING:
                return <string>{value.StringValue}</string>;
            case flockIXmlRpcValue.TYPE_DOUBLE:
                return <double>{String(value.DoubleValue)}</double>;
            case flockIXmlRpcValue.TYPE_DATETIME:
                var date = new Date ();
                date.setTime (value.DateTimeValue * 1000);

                var datetime = date.getUTCFullYear();
                var month = String(date.getUTCMonth() + 1);
                datetime += (month.length == 1 ?  '0' + month : month);
                var day = date.getUTCDate();
                datetime += (day < 10 ? '0' + day : day);

                datetime += 'T';

                var hour = date.getUTCHours();
                datetime += (hour < 10 ? '0' + hour : hour) + ':';
                var minutes = date.getUTCMinutes();
                datetime += (minutes < 10 ? '0' + minutes : minutes) + ':';
                var seconds = date.getUTCSeconds();
                datetime += (seconds < 10 ? '0' + seconds : seconds);

                return <dateTime.iso8601>{datetime}</dateTime.iso8601>;
            case flockIXmlRpcValue.TYPE_ARRAY:
                var elements = value.ArrayElements ({});
                var array = <array><data/></array>;
                for (var i=0; i<elements.length; i++) {
                    var q = XmlRpcXml.build (elements[i]);
                    if (q == null) {
                        continue;
                    }
                    array.data.foo = <value>{q}</value>;
                }
                return array;
            case flockIXmlRpcValue.TYPE_STRUCT:
                var keys = value.StructKeys ({});
                var struct = <struct/>;
                for (var i in keys) {
                    var q = XmlRpcXml.build (value.StructItem(keys[i]));
                    if (q == null) {
                        continue;
                    }
                    struct.foo = <member>
                                     <name>{keys[i]}</name>
                                     <value>{q}</value>
                                 </member>;
                }
                return struct;
            case flockIXmlRpcValue.TYPE_BASE64:
                return <base64>{b64encode(value.Base64Value)}</base64>;
            default:
                throw 'error building XML';
                return null;
        }
    },
    parse: function (xml) {
        DEBUG ('XmlRpcXml.parse: '+xml);
        if (xml == undefined) {
            // this is the empty string
            return '';
        }
        if (xml.nodeKind() == 'text') {
            // the default type in string
            return XmlRpcValue.String (xml.toString());
        }
        switch (xml.name().toString()) {
            case 'int':
            case 'i4':
                return XmlRpcValue.Int (parseInt (xml.text()));
            case 'boolean':
                return XmlRpcValue.Boolean (parseInt (xml.text()) == 1);
            case 'string':
                return XmlRpcValue.String (xml.text().toString());
            case 'double':
                return XmlRpcValue.Double (parseFloat (xml.text()));
            case 'dateTime.iso8601':
                var val = xml.text().toString();
                var dateutc =  Date.UTC(val.slice(0, 4), val.slice(4, 6) - 1, 
                        val.slice(6, 8), val.slice(9, 11), val.slice(12, 14), 
                        val.slice(15));
                return XmlRpcValue.DateTime (new Date(dateutc));
            case 'array':
                var arr = new Array ();
                for (var i=0; i<xml.data.value.length(); i++) {
                    arr.push (XmlRpcXml.parse(xml.data.value[i].children()[0]));
                }
                return XmlRpcValue.Array (arr);
            case 'struct':
                var struct = new Object ();
                for (var i=0; i<xml.member.length(); i++) {
                    struct[xml.member[i].name.text()] = 
                        XmlRpcXml.parse(xml.member[i].value.children()[0]);
                }
                return XmlRpcValue.Struct (struct);
            case 'base64':
                return XmlRpcValue.Base64 (b64decode (xml.text().toString()));
            default:
                throw 'error parsing XML';
        }
    },
}



// flockXmlRpcServer object
function flockXmlRpcServerComponent () {
    this.mURL = null;
    this.mTimeout = null;
}

flockXmlRpcServerComponent.prototype.QueryInterface = function (aIID) {
    if (aIID.equals (Components.interfaces.flockIXmlRpcServer) ||
            aIID.equals (Components.interfaces.nsISupports)) {
        return this;
    }

    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}

flockXmlRpcServerComponent.prototype.init = function (aURL) {
    DEBUG ('flockXmlRpcServer.init ("'+aURL+'")');
    this.mURL = aURL;
    this.mTimeout = 120*1000;
}

flockXmlRpcServerComponent.prototype.call = function (aMethod, aArguments, aListener) {
    //var aOptionalTimeoutInSeconds = 10;
    DEBUG ('flockXmlRpcServer.call ('+aMethod+', '+aArguments+', '+aListener+')\n');
    if (aArguments.XmlRpcType != flockIXmlRpcValue.TYPE_ARRAY) {
        aListener.onError ('the arguments must be in an xmrpc array');
        return;
    }
    var methodCall = <methodCall/>;
    methodCall.methodName = aMethod;

    var arguments = aArguments.ArrayElements ({});
    DEBUG ('XMLRPC: got '+arguments.length+' arguments');
    for (var i in arguments) {
        DEBUG ('XMLRPC: adding arg '+XmlRpcXml.build (arguments[i]));
        methodCall.params.param += <param><value>{XmlRpcXml.build (arguments[i])}</value></param>;
    }
    

       
    var url = this.mURL;
    var timeout = this.mTimeout;
    var callback = new Object ();
    callback.notify = function (timer) {
        var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance (Components.interfaces.nsIXMLHttpRequest);
        xhr.open ('POST', url, true);
        xhr.setRequestHeader ('Content-Type', 'text/xml');

        // FIXME: what about charsets?
        // FIXME: should we make this configurable?
        xhr.overrideMimeType ('text/xml'); 

        xhr.onreadystatechange = function (aEvent) {
            if (xhr.readyState == 4) {
                if (!xhr.responseXML) {
                    aListener.onError (xhr.responseText);
                    return;
                }
 
                if (xhr.status != 200) {
                    aListener.onError (xhr.statusText);
                    return;
                }

                //save the state of the XML object directives
                Orig_XML_ignoreComments  = XML.ignoreComments;
                Orig_XML_ignoreProcessingInstructions  = XML.ignoreProcessingInstructions;
                Orig_XML_ignoreWhitespace = XML.ignoreWhitespace;
                Orig_XML_prettyPrinting = XML.prettyPrinting;
                Orig_XML_prettyIndent = XML.prettyIndent;
                //set them to what we need for the parser to behave
                XML.ignoreComments  = true;
                XML.ignoreProcessingInstructions  = true;
                XML.ignoreWhitespace = true;
                XML.prettyPrinting = false;
                XML.prettyIndent = false;
                //var response = new XML (xhr.responseXML);
                var response = new XML (Components.classes['@mozilla.org/xmlextras/xmlserializer;1'].createInstance (Components.interfaces.nsIDOMSerializer).serializeToString(xhr.responseXML.documentElement));
                if (response.name() != 'methodResponse' ||
                        !(response.params.param.value.length() == 1 ||
                            response.fault.value.struct.length() == 1)) {
                    aListener.onError ('invalid response XML');
                    return;
                }

                if (response.params.param.value.length() == 1) {
                    aListener.onResult (XmlRpcXml.parse (response.params.param.value.children()[0]));
                    //put them back like we found them before we return
                    XML.ignoreComments = Orig_XML_ignoreComments;
                    XML.ignoreProcessingInstructions = Orig_XML_ignoreProcessingInstructions;
                    XML.ignoreWhitespace = Orig_XML_ignoreWhitespace;
                    XML.prettyPrinting = Orig_XML_prettyPrinting;
                    XML.prettyIndent = Orig_XML_prettyIndent;
                    return;
                }

                var fault = XmlRpcValue.ToJavaScript (
                        XmlRpcXml.parse (response.fault.value.struct));
                if (fault['faultCode'] == undefined ||
                        fault['faultString'] == undefined) {
                    aListener.onError ('invalid response XML');
                    //put them back like we found them before we return
                    XML.ignoreComments = Orig_XML_ignoreComments;
                    XML.ignoreProcessingInstructions = Orig_XML_ignoreProcessingInstructions;
                    XML.ignoreWhitespace = Orig_XML_ignoreWhitespace;
                    XML.prettyPrinting = Orig_XML_prettyPrinting;
                    XML.prettyIndent = Orig_XML_prettyIndent;
                    return;
                }

                aListener.onFault (fault['faultcode'], fault['faultString']);
                //put them back like we found them before we return
                XML.ignoreComments = Orig_XML_ignoreComments;
                XML.ignoreProcessingInstructions = Orig_XML_ignoreProcessingInstructions;
                XML.ignoreWhitespace = Orig_XML_ignoreWhitespace;
                XML.prettyPrinting = Orig_XML_prettyPrinting;
                XML.prettyIndent = Orig_XML_prettyIndent;
                return;
            }
        }
        DEBUG ('XMLRPC: sending: '+'<?xml version="1.0"?>\n'+methodCall.toXMLString ());
        DEBUG ('XMLRPC timeout is "' + timeout + '"');
        var timeoutCallback= new Object();
        timeoutCallback.notify = function() {
            DEBUG ('timeoutCallback.notify called');
            if (xhr == null) {
                DEBUG ("flockXmlRpcServerComponent.prototype.call.timeoutCallback Called with null request!!");
            } else if ((xhr != null) && (xhr.readyState != 4)) {
                DEBUG ('XMLRPC timed out');
                //the request still exists and hasn't completed
                delete xhr['onreadystatechange'];     //Kill the callback function
                xhr.abort();                          //abort the request
                aListener.onError ('timeout');          //return an error to the caller
            }
        }
        var timeOutTimer = Components.classes[TIMER_CONTRACTID].createInstance(nsITimer);
        timeOutTimer.initWithCallback (timeoutCallback, timeout, nsITimer.TYPE_ONE_SHOT);
        xhr.send ('<?xml version="1.0"?>\n'+methodCall.toXMLString ());
    }
    var timer = Components.classes[TIMER_CONTRACTID].createInstance(nsITimer);
    timer.initWithCallback (callback, 1, nsITimer.TYPE_ONE_SHOT);
}





function loadLibrary(aFilename) { // {{{
    var file = Components.classes["@mozilla.org/file/directory_service;1"]
        .getService(Components.interfaces.nsIProperties)
        .get("ComsD", Components.interfaces.nsIFile);
    file.append(aFilename);

    var ios = Components.classes["@mozilla.org/network/io-service;1"]
        .getService(Components.interfaces.nsIIOService);
    var fileHandler = ios.getProtocolHandler("file")
        .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
    var spec = fileHandler.getURLSpecFromFile(file);

    var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
        .getService(Components.interfaces.mozIJSSubScriptLoader);

    scriptLoader.loadSubScript(spec);
} // }}}

// load the XPCOM library
loadLibrary("flockXPCOMTemplate.js.lib");

// load the XMLRPC helper
loadLibrary ('flockXmlRpcHelper.js.lib');

// JavaScript XPCOM Boilerplate
function xpcomModule (aCID, aContractId, aComponentName, aConstructor) {
    this.mCID = Components.ID (aCID);
    this.mContractId = aContractId;
    this.mComponentName = aComponentName;
    this.mConstructor = aConstructor;
    this.mGlobalJavaScript = null;

    // factory object
    this.mFactory = {
        constructor: this.mConstructor,
        createInstance: function (aOuter, aIID) {
            if (aOuter != null) {
                throw Components.results.NS_ERROR_NO_AGGREGATION;
            }

            return (new (this.constructor) ()).QueryInterface (aIID);
        }
    };

}
xpcomModule.prototype = {
    setGlobalJavaScript: function (aGlobalJavaScript) {
        this.mGlobalJavaScript = aGlobalJavaScript;
    },

    // the module should register itself
    registerSelf: function (aCompMgr, aLocation, aLoaderStr, aType) {
        aCompMgr = aCompMgr.QueryInterface (
                Components.interfaces.nsIComponentRegistrar);
        aCompMgr.registerFactoryLocation (this.mCID, this.mComponentName, 
                this.mContractId, aLocation, aLoaderStr, aType);
        var catmgr = Components.classes["@mozilla.org/categorymanager;1"]
            .getService (Components.interfaces.nsICategoryManager);
        if (this.mGlobalJavaScript) {
            catmgr.addCategoryEntry ("JavaScript global property",
                    this.mGlobalJavaScript, this.mContractId, true, true);
        }
    },

    // get the factory                  
    getClassObject: function (aCompMgr, aCID, aIID) {
        if (!aCID.equals (this.mCID)) {
            throw Components.results.NS_ERROR_NO_INTERFACE;
        }

        if (!aIID.equals (Components.interfaces.nsIFactory)) {
            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        }

        return this.mFactory;
    },

    canUnload: function(compMgr) {
        return true;
    }
};


var module = new xpcomModule ('{709f57f4-a4e4-41bb-a38d-86309fbc4858}',
        '@flock.com/xmlrpc/server;1', 'Flock XML-RPC', 
        flockXmlRpcServerComponent);
function NSGetModule(aCompMgr, aFileSpec) {
    return module;
};
