// $Id: xmlrpc.js 86 2008-12-12 03:15:20Z x2iiinf $
// Copyright (c) 2004 Kurt M. Brown, x2ii.info.
// This file is subject to the terms and conditions of the GNU General Public
// License. See the file COPYING for more details.

xi.xmlrpc = {}; // namespace xmlrpc
xi.xmlrpc.relative = '/../../';
xi.xmlrpc.defaultOnFault = function(fault) {
    xi.error('xi.xmlrpc.send().onFault()', fault);
}

xi.postDoc = function(xRequestDoc, handler)
{
    var requestor = new XMLHttpRequest();
    requestor.abort();
    var href = new String(location.href);
    var q = href.indexOf('?');
    if (q > -1) // remove query-string
        href = href.substr(0, q);
    requestor.open('post', href + xi.xmlrpc.relative + handler.path, true);
    requestor.onreadystatechange = function() {
        if (requestor.readyState != 4)
            return;
        var xResponseDoc = requestor.responseXML;
        if (xResponseDoc == null && !xi.isEmpty(requestor.responseText)) {
            // if status == 500, then we must use the text
            xResponseDoc = xi.dom.create(requestor.responseText);
            xResponseDoc.normalize();
        }
        const status = requestor.status;
        requestor = null;
        if (xResponseDoc == null)
            alert('error: postDoc.stateChange, ' + status);
        else {
            var response = handler.unwrap(xResponseDoc);
            xResponseDoc = null;
            if (status == 200)
                handler.onSuccess(response);
            else
                handler.onFault(handler.createFault(response));
        }
    }
    requestor.send(handler.wrap(xRequestDoc));
}

///////////////
//  Handler  //
///////////////

xi.xmlrpc.Handler = function(onSuccess, onFault)
{
    this.path = 'handler.php';
    this.$onSuccess = onSuccess;
    this.$onFault = onFault;
    this.$processor = new xi.xmlrpc.Processor();
}
xi.xmlrpc.Handler.prototype.onSuccess = function(params)
{
    this.$onSuccess(params);
}
xi.xmlrpc.Handler.prototype.onFault = function(fault)
{
    const isCorrectVersion = (fault.faultCode != 'client.wrong-version');
    if (isCorrectVersion)
        this.$onFault(fault);
    return isCorrectVersion;
}
xi.xmlrpc.Handler.prototype.wrap = function(xRequestDoc)
{
    return xRequestDoc;
}
xi.xmlrpc.Handler.prototype.unwrap = function(xResponseDoc)
{
    if (xResponseDoc.documentElement.firstChild.tagName == 'fault')
        return this.$processor.decodeFault(xResponseDoc);
    else
        return this.$processor.decodeResponse(xResponseDoc);
}
xi.xmlrpc.Handler.prototype.createFault = function(faultStruct)
{
    return new xi.xmlrpc.Fault(faultStruct);
}
xi.xmlrpc.Handler.prototype.post = function(methodName, params)
{
    xi.postDoc(this.$processor.createRequestDoc(methodName, params), this);
}

/////////////
//  Fault  //
/////////////

xi.xmlrpc.Fault = function(faultStruct)
{
    this.faultCode = faultStruct.faultCode;
    this.faultString = faultStruct.faultString;
}
xi.xmlrpc.Fault.prototype.toString = function()
{
    return this.faultCode + ', ' + this.faultString;
}

////////////
//  send  //
////////////

xi.xmlrpc.send = function(methodName, params, onSuccess, onFault)
{
    if (!onFault)
        onFault = xi.xmlrpc.defaultOnFault;
    var handler = new xi.xmlrpc.Handler(onSuccess, onFault);
    handler.post(methodName, params);
}

xi.xmlrpc.test = function()
{
    function onSuccess(params) {
        xi.dprint('xmlrpc.test()', xi.dprint(params));
    }
    function onFault(fault) {
        xi.dprint('xmlrpc.test(), fault', fault);
    }
    var params = [
        'a string',
        new Number(123),
        new Number(55.55123),
        ['a1', 'a2'],
        new Boolean(true),
        {
            gogo : 123,
            noto : false,
            xoto : 555.444,
            now : new Date()
        },
        xi.dom.create('<test v="1"><subtest v="2"/></test>')
    ];
    xi.xmlrpc.send('system.echo', params, onSuccess);
}

/////////////////
//  Processor  //
/////////////////

xi.xmlrpc.Processor = function()
{
}

xi.xmlrpc.Processor.prototype.createRequestDoc = function(methodName, params)
{
    var xDoc = xi.dom.create('<methodCall/>');
    var xRequest = xDoc.documentElement;
    xRequest.appendChild(this.p_createElement(xDoc, 'methodName', methodName));
    if (params.length > 0) {
        var xParams = xRequest.appendChild(xDoc.createElement('params'));
        for (var index in params) {
            var xParam = xParams.appendChild(xDoc.createElement('param'));
            this.p_encodeValue(xDoc, xParam, params[index]);
        }
    }
    return xDoc;
}
xi.xmlrpc.Processor.prototype.decodeResponse = function(xResponseDoc)
{
    var params = [];

    var xparams = xResponseDoc.selectNodes("//param");
    for (var i = 0; i < xparams.length; ++i)
        params.push(this.p_decodeValue(xparams[i].firstChild.firstChild));

    return params;
}
xi.xmlrpc.Processor.prototype.decodeFault = function(xFaultDoc)
{
    var xFault = xFaultDoc.selectSingleNode("//fault");
    // <fault><value><struct>
    return this.p_decodeValue(xFault.firstChild.firstChild);
}

xi.xmlrpc.Processor.prototype.p_encodeValue = function(xDoc, xParent, param)
{
    const type = this.p_type(param);
    if (type == 'unknown')
        return;

    var xValue = xParent.appendChild(xDoc.createElement('value'));
    if (type == 'i4')
        xValue.appendChild(this.p_createElement(xDoc, 'i4', param));
    else if (type == 'double')
        xValue.appendChild(this.p_createElement(xDoc, 'double', param));
    else if (type == 'boolean')
        xValue.appendChild(this.p_createElement(xDoc, 'boolean', param ? 1 : 0));
    else if (type == 'string')
        xValue.appendChild(this.p_createElement(xDoc, 'string', param));
    else if (type == 'array')
        this.p_encodeArray(xDoc, xValue, param);
    else if (type == 'node') {
        var xNode = xValue.appendChild(xDoc.createElement('node'));
        xNode.appendChild(xDoc.importNode(param.documentElement, true));
    }
    else if (type == 'struct')
        this.p_encodeStruct(xDoc, xValue, param);
    else if (type == 'timestamp')
        xValue.appendChild(
            this.p_createElement(xDoc, 'timestamp', xi.util.timestamp(param))
        );
    else if (type == 'base64')
        xValue.appendChild(this.p_createElement(xDoc, 'base64', param.encode()));
}
xi.xmlrpc.Processor.prototype.p_encodeArray = function(xDoc, xValue, arr)
{
    var xArray = xValue.appendChild(xDoc.createElement('array'));
    var xData = xArray.appendChild(xDoc.createElement('data'));
    for (index in arr)
        this.p_encodeValue(xDoc, xData, arr[index]);
}
xi.xmlrpc.Processor.prototype.p_encodeStruct = function(xDoc, xValue, struct)
{
    var xStruct = xValue.appendChild(xDoc.createElement('struct'));
    for (name in struct) {
        var xMember = xStruct.appendChild(xDoc.createElement('member'));
        xMember.appendChild(this.p_createElement(xDoc, 'name', name));
        this.p_encodeValue(xDoc, xMember, struct[name]);
    }
}

xi.xmlrpc.Processor.prototype.p_type = function(value)
{
    var type = typeof(value);
    switch (type) {
        case 'string':
        case 'boolean':
            break;
        case 'number':
            type = (Math.round(value) == value) ? 'i4' : 'double';
            break;
        case 'object':
            if (value.hasOwnProperty('xmlrpcType'))
                type = value.xmlrpcType;
            else if (value instanceof Number)
                type = (Math.round(value) == value) ? 'i4' : 'double';
            else if (value instanceof Boolean)
                type = 'boolean';
            else if (value instanceof Date)
                type = 'timestamp';
            else if (value instanceof Array)
                type = 'array';
            else if (value instanceof Document)
                type = 'node';
            else if (value instanceof Object)
                type = 'struct';
            else {
                xi.dprint('obj, unknown instance', type, value.constructor);
                type = 'unknown';
            }
            break;
        default:
            xi.dprint('unknown type', type);
            type = 'unknown';
            break;
  }
  return type;
}
xi.xmlrpc.Processor.prototype.p_createElement = function(xDoc, tagName, value)
{
    var xElem = xDoc.createElement(tagName);
    xElem.appendChild(xDoc.createTextNode(value));
    return xElem;
}
xi.xmlrpc.Processor.prototype.p_decodeValue = function(xType)
{
    var val = null;

    if (xType.nodeType == xType.TEXT_NODE)
        val = xType.textContent;
    else {
        const tagName = xType.tagName;
        if (tagName == 'string') {
            // textContent trims whitespace, which we do not want.
            if (xType.firstChild)
                val = xType.firstChild.nodeValue;
            else
                val = xType.textContent;
        }
        else if (tagName == 'i4')
            val = parseInt(xType.textContent, 10);
        else if (tagName == 'boolean')
            val = xType.textContent === '1' ? true : false;
        else if (tagName == 'double')
            val = parseFloat(xType.textContent);
        else if (tagName == 'timestamp') // non-standard
            val = new Date(1000*xType.textContent);
        else if (tagName == 'array')
            val = this.p_decodeArray(xType.firstChild);
        else if (tagName == 'node') {
            val = document.implementation.createDocument('', '', null);
            val.appendChild(val.importNode(xType.firstChild, true));
        }
        else if (tagName == 'struct')
            val = this.p_decodeStruct(xType);
        else if (tagName == 'base64')
            val = new xi.Base64(xType.textContent);
        else if (tagName == 'date')
            val = Date.parse(xType.textContent);
    }
    return val;
}
xi.xmlrpc.Processor.prototype.p_decodeArray = function(xData)
{
    var arr = new Array();

    var xvalues = xData.selectNodes('value');
    for (var i = 0; i < xvalues.length; ++i)
        arr.push(this.p_decodeValue(xvalues[i].firstChild));

    return arr;
}
xi.xmlrpc.Processor.prototype.p_decodeStruct = function(xStruct)
{
    var struct = {};

    var xmembers = xStruct.selectNodes('member');
    for (var i = 0; i < xmembers.length; ++i) {
        var xMember = xmembers[i];
        name = xMember.firstChild.textContent;
        struct[name] = this.p_decodeValue(xMember.lastChild.firstChild);
    }
    return struct;
}

