// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms 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 CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;


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

CU.import("resource:///modules/FlockXPCOMUtils.jsm");
CU.import("resource:///modules/ISO8601DateUtils.jsm");

const FLOCK_XMLRPC_CLASS_NAME = "Flock XML-RPC";
const FLOCK_XMLRPC_CLASS_ID =
  Components.ID("{709f57f4-a4e4-41bb-a38d-86309fbc4858}");
const FLOCK_XMLRPC_CONTRACT_ID = "@flock.com/xmlrpc/server;1";

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;
  }
}


var XmlRpcXml = {
  build: function XRX_build(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 = ISO8601DateUtils.create(date);
        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>{ Base64.encode(value.Base64Value) }</base64>;
      default:
        throw "error building XML";
        return null;
    }
  },
  parse: function XRX_parse(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 myDate = ISO8601DateUtils.parse(val);
        return XmlRpcValue.DateTime(myDate);
      case "array":
        var arr = [];
        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 = {};
        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(Base64.decode(xml.text().toString()));
      default:
        throw "error parsing XML";
    }
  }
}



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

flockXmlRpcServerComponent.prototype = new FlockXPCOMUtils.genericComponent(
  FLOCK_XMLRPC_CLASS_NAME,
  FLOCK_XMLRPC_CLASS_ID,
  FLOCK_XMLRPC_CONTRACT_ID,
  flockXmlRpcServerComponent,
  0, // nsIClassInfo flags
  [CI.flockIXmlRpcServer]
);

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

flockXmlRpcServerComponent.prototype.call =
function XRSC_call(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 = {};
  callback.notify = function XRSC_call_callback_notify() {
    var xhr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
              .createInstance(CI.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 XRSC_call_xhr(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 serializer = CC["@mozilla.org/xmlextras/xmlserializer;1"]
                         .createInstance(CI.nsIDOMSerializer);
        var xmlStr =
          serializer.serializeToString(xhr.responseXML.documentElement);

        var response = new XML(xmlStr);

        if (response.name() != "methodResponse" ||
            !(response.params.param.value.length() == 1 ||
            response.fault.value.struct.length() == 1))
        {
          aListener.onError("invalid response XML: " + xhr.responseText);
          return;
        }

        if (response.params.param.value.length() == 1) {
          var resp = XmlRpcXml.parse(response.params.param.value.children()[0]);
          aListener.onResult(resp);
          //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: " + xhr.responseText);
          //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= {};
    timeoutCallback.notify = function XRSC_call_timeout_notify() {
      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 = CC["@mozilla.org/timer;1"]
                       .createInstance(CI.nsITimer);
    timeoutTimer.initWithCallback(timeoutCallback, timeout,
                                  CI.nsITimer.TYPE_ONE_SHOT);

    xhr.send('<?xml version="1.0"?>\n' + methodCall.toXMLString());
  }

  var timer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
  timer.initWithCallback(callback, 1, CI.nsITimer.TYPE_ONE_SHOT);
}


// load the XMLRPC helper
var loader = CC['@mozilla.org/moz/jssubscript-loader;1']
             .getService(CI.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://flock/content/xmlrpc/xmlrpchelper.js");

var NSGetModule =
  FlockXPCOMUtils.generateNSGetModule(FLOCK_XMLRPC_CLASS_NAME,
                                      [flockXmlRpcServerComponent]);
