/*
 * libMaia - maiaObject.cpp
 * Copyright (c) 2003 Frerich Raabe <raabe@kde.org> and
 *                    Ian Reinhart Geiser <geiseri@kde.org>
 * Copyright (c) 2007 Sebastian Wiedenroth <wiedi@frubar.net>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "maiaObject.h"
#include "appglobal.h"

MaiaObject::MaiaObject(QObject* parent) : QObject(parent)
{
    QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars);
}

QDomElement MaiaObject::toXml(QVariant arg)
{

    //dummy document
    QDomDocument doc;
    //value element, we need this in each case

    if(arg.isNull())
    {
        QDomElement tagString = doc.createElement("value");
        QDomText textString = doc.createTextNode("");

        //        tagValue.appendChild(tagString);
        tagString.appendChild(textString);

        return tagString;
    }

    QDomElement tagValue = doc.createElement("value");
    switch(arg.type())
    {
    case QVariant::String:
    {

        QDomElement tagString = doc.createElement("string");
        QDomText textString = doc.createTextNode(arg.toString());

        tagValue.appendChild(tagString);
        tagString.appendChild(textString);

        return tagValue;

    }
    case QVariant::Int:
    {

        QDomElement tagInt = doc.createElement("int");
        QDomText textInt = doc.createTextNode(QString::number(arg.toInt()));

        tagValue.appendChild(tagInt);
        tagInt.appendChild(textInt);

        return tagValue;

    }
    case QVariant::Double:
    {

        QDomElement tagDouble = doc.createElement("double");
        QDomText textDouble = doc.createTextNode(QString::number(arg.toDouble(),'g',9));

        tagValue.appendChild(tagDouble);
        tagDouble.appendChild(textDouble);

        return tagValue;

    }
    case QVariant::Bool:
    {

        QString textValue = arg.toBool() ? "1" : "0";

        QDomElement tag = doc.createElement("boolean");
        QDomText text = doc.createTextNode(textValue);

        tagValue.appendChild(tag);
        tag.appendChild(text);

        return tagValue;

    }
    case QVariant::ByteArray:
    {

        QString textValue = arg.toByteArray().toBase64();

        QDomElement tag = doc.createElement("base64");
        QDomText text = doc.createTextNode(textValue);

        tagValue.appendChild(tag);
        tag.appendChild(text);

        return tagValue;

    }
    case QVariant::DateTime:
    {

        QString textValue = arg.toDateTime().toString("yyyyMMddThh:mm:ss");

        QDomElement tag = doc.createElement("datetime.iso8601");
        QDomText text = doc.createTextNode(textValue);

        tagValue.appendChild(tag);
        tag.appendChild(text);

        return tagValue;

    }
    case QVariant::List: {

        QDomElement tagArray = doc.createElement("array");
        QDomElement tagData = doc.createElement("data");
        tagArray.appendChild(tagData);
        tagValue.appendChild(tagArray);

        const QList<QVariant> args = arg.toList();
        for(int i = 0; i < args.size(); ++i) {
            tagData.appendChild(toXml(args.at(i)));
        }

        return tagValue;

    }
    case QVariant::Map:
    {

        QDomElement tagStruct = doc.createElement("struct");
        QDomElement member;
        QDomElement name;

        tagValue.appendChild(tagStruct);

        QMap<QString, QVariant> map = arg.toMap();
        QMapIterator<QString, QVariant> i(map);
        while(i.hasNext())
        {
            i.next();

            member = doc.createElement("member");
            name = doc.createElement("name");

            // (key) -> name -> member -> struct
            tagStruct.appendChild(member);
            member.appendChild(name);
            name.appendChild(doc.createTextNode(i.key()));

            // add variables by recursion
            member.appendChild(toXml(i.value()));
        }

        return tagValue;

    }
    default:
        errorOut() << "Failed to marshal unknown variant type: " << arg.type() << endl;
    }
    return QDomElement(); //QString::null;
}

QVariant MaiaObject::fromXml(const QDomElement &elem)
{
    if(elem.tagName().toLower() != "value")
    {
        return QVariant();
    }

    // If no type is indicated, the type is string.
    if(!elem.firstChild().isElement())
    {
        return QVariant(elem.text());
    }

    const QDomElement typeElement = elem.firstChild().toElement();
    const QString typeName = typeElement.tagName().toLower();

    if(typeName == "string") return QVariant(typeElement.text());
    else if(typeName == "i4" || typeName == "int")
      {
        return QVariant(typeElement.text().toInt());
      }
    else if(typeName == "double") return QVariant(typeElement.text().toDouble());
    else if (typeName == "boolean")
    {
        if(typeElement.text().toLower() == "true" || typeElement.text() == "1") return QVariant(true);
        else  return QVariant(false);
    }
    else if(typeName == "base64") return QVariant(QByteArray::fromBase64( typeElement.text().toLatin1()));
    else if(typeName == "datetime" || typeName == "datetime.iso8601") return QVariant(QDateTime::fromString(typeElement.text(), "yyyyMMddThh:mm:ss"));
    else if(typeName == "nil")  return QVariant(); // Non-standard extension: http://ontosys.com/xml-rpc/extensions.php
    else if ( typeName == "array" )
    {
        QList<QVariant> values;
        QDomNode valueNode = typeElement.firstChild().firstChild();
        while(!valueNode.isNull())
        {
            values << fromXml(valueNode.toElement());
            valueNode = valueNode.nextSibling();
        }
        return QVariant(values);
    }
    else if ( typeName == "struct" )
    {
        QMap<QString, QVariant> map;
        QDomNode memberNode = typeElement.firstChild();
        while(!memberNode.isNull())
        {
            const QString key = memberNode.toElement().elementsByTagName("name").item(0).toElement().text();
            const QVariant data = fromXml(memberNode.toElement().elementsByTagName("value").item(0).toElement());
            map[key] = data;
            memberNode = memberNode.nextSibling();
        }
        return QVariant(map);
    }
    else
    {
        errorOut() << "Cannot demarshal unknown type " << typeElement.tagName().toLower();
    }
    return QVariant();
}


QString MaiaObject::prepareCall(QString method, QList<QVariant> args) {


    QDomDocument doc;

    QDomProcessingInstruction header = doc.createProcessingInstruction( "xml", QString("version=\"1.0\" encoding=\"UTF-8\"" ));
    doc.appendChild(header);

    QDomElement methodCall = doc.createElement("methodCall");
    QDomElement methodName = doc.createElement("methodName");
    QDomElement params = doc.createElement("params");
    QDomElement param;

    doc.appendChild(methodCall);
    methodCall.appendChild(methodName);
    methodName.appendChild(doc.createTextNode(method));

    methodCall.appendChild(params);

    for(int i = 0; i < args.size(); ++i) {
        param = doc.createElement("param");
        param.appendChild(toXml(args.at(i)));
        params.appendChild(param);
    }

    return doc.toString();
}

QString MaiaObject::prepareResponse(QVariant arg) {

    QDomDocument doc;

    QDomProcessingInstruction header = doc.createProcessingInstruction( "xml", QString("version=\"1.0\" encoding=\"UTF-8\"" ));
    doc.appendChild(header);

    QDomElement methodResponse = doc.createElement("methodResponse");
    QDomElement params = doc.createElement("params");
    QDomElement param;

    doc.appendChild(methodResponse);

    methodResponse.appendChild(params);

    //    if(!arg.isNull())
    {
        param = doc.createElement("param");
        param.appendChild(toXml(arg));
        params.appendChild(param);
    }
    return doc.toString(-1);
}

void MaiaObject::parseResponse(QString response, QNetworkReply* reply) {
    QDomDocument doc;
    QVariant arg;
    QString errorMsg;
    int errorLine;
    int errorColumn;
    if(!doc.setContent(response, &errorMsg, &errorLine, &errorColumn)) {
        emit fault(-32700, QString("parse error: response not well formed at line %1: %2").arg(errorLine).arg(errorMsg), reply);
        delete this;
        return;
    }
    if(doc.documentElement().firstChild().toElement().tagName().toLower() == "params") {
        QDomNode paramNode = doc.documentElement().firstChild().firstChild();
        if(!paramNode.isNull()) {
            arg = fromXml( paramNode.firstChild().toElement() );
        }
        emit aresponse(arg, reply);
    } else if(doc.documentElement().firstChild().toElement().tagName().toLower() == "fault") {
        const QVariant errorVariant = fromXml(doc.documentElement().firstChild().firstChild().toElement());
        emit fault(errorVariant.toMap() [ "faultCode" ].toInt(),
                errorVariant.toMap() [ "faultString" ].toString(),
                reply);
    } else {
        emit fault(-32600,
                   tr("parse error: invalid xml-rpc. not conforming to spec."),
                   reply);
    }
    delete this;
    return;
}

