/****************************************************************************
**
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
****************************************************************************/


#include <QDebug>
#include <QtCore>
#include <poll.h>
#include <dbus/dbus.h>
#include <dbus/dbus-glib-lowlevel.h>
#include <glib.h>
#include "dbusdispatcher.h"

namespace Maemo {

/*!
    \class Maemo::DBusDispatcher

    \brief DBusDispatcher is a class that can send DBUS method call
    messages and receive unicast signals from DBUS objects.
*/

class DBusDispatcherPrivate
{
public:
    DBusDispatcherPrivate(const QString& service,
                          const QString& path,
                          const QString& interface,
                          const QString& signalPath)
        : service(service), path(path), interface(interface),
          signalPath(signalPath), connection(0)
    {
        memset(&signal_vtable, 0, sizeof(signal_vtable));
    }

    ~DBusDispatcherPrivate()
    {
        foreach(DBusPendingCall *call, pending_calls) {
            dbus_pending_call_cancel(call);
            dbus_pending_call_unref(call);
        }
    }

    QString service;
    QString path;
    QString interface;
    QString signalPath;
    struct DBusConnection *connection;
    QList<DBusPendingCall *> pending_calls;
    struct DBusObjectPathVTable signal_vtable;
};

static bool constantVariantList(const QVariantList& variantList) {
    // Special case, empty list == empty struct
    if (variantList.isEmpty()) {
        return false;
    } else {        
        QVariant::Type type = variantList[0].type();
        // Iterate items in the list and check if they are same type
        foreach(QVariant variant, variantList) {
            if (variant.type() != type) {
                return false;
            }
        }
    }
    return true;
}

static QString variantToSignature(const QVariant& argument,
                                  bool constantList = true) {
    switch (argument.type()) {
        case QVariant::Bool:
            return "b";
        case QVariant::ByteArray:
            return "ay";
        case QVariant::Char:
            return "y";
        case QVariant::Int:
            return "i";
        case QVariant::UInt:
            return "u";
        case QVariant::StringList:
            return "as";
        case QVariant::String:
            return "s";
        case QVariant::LongLong:
            return "x";
        case QVariant::ULongLong:
            return "t";
        case QVariant::List:
            {
            QString signature;
            QVariantList variantList = argument.toList();
            if (!constantList) {
                signature += DBUS_STRUCT_BEGIN_CHAR_AS_STRING;
                foreach(QVariant listItem, variantList) {
                    signature += variantToSignature(listItem);
                }
                signature += DBUS_STRUCT_END_CHAR_AS_STRING;
            } else {
                if (variantList.isEmpty())
                    return "";
                signature = "a" + variantToSignature(variantList[0]);
            }

            return signature;
            }
        default:
            qDebug() << "Unsupported variant type: " << argument.type();
            break;
    }

    return "";
}

static bool appendVariantToDBusMessage(const QVariant& argument,
                                       DBusMessageIter *dbus_iter) {
    int idx = 0;
    DBusMessageIter array_iter;
    QStringList str_list;
    dbus_bool_t bool_data;
    dbus_int32_t int32_data;
    dbus_uint32_t uint32_data;
    dbus_int64_t int64_data;
    dbus_uint64_t uint64_data;
    char *str_data;
    char char_data;

    switch (argument.type()) {

        case QVariant::Bool:
            bool_data = argument.toBool();
            dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_BOOLEAN, 
                                           &bool_data);
            break;

        case QVariant::ByteArray:
            str_data = argument.toByteArray().data();
            dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_ARRAY,
                                             DBUS_TYPE_BYTE_AS_STRING, &array_iter);
            dbus_message_iter_append_fixed_array(&array_iter,
                                                 DBUS_TYPE_BYTE,
                                                 &str_data,
                                                 argument.toByteArray().size());
            dbus_message_iter_close_container(dbus_iter, &array_iter);
            break;

        case QVariant::Char:
            char_data = argument.toChar().toAscii();
            dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_BYTE, 
                                           &char_data);
            break;

        case QVariant::Int:
            int32_data = argument.toInt();
            dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_INT32, 
                                           &int32_data);
            break;

        case QVariant::String: {
            QByteArray data = argument.toString().toUtf8();
            str_data = data.data();
            dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_STRING,
                                           &str_data);
            break;
        }

        case QVariant::StringList:
            str_list = argument.toStringList();
            dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_ARRAY,
                                             "s", &array_iter);
            for (idx = 0; idx < str_list.size(); idx++) {
                QByteArray data = str_list.at(idx).toLatin1();
                str_data = data.data();
                dbus_message_iter_append_basic(&array_iter,
                                               DBUS_TYPE_STRING,
                                               &str_data);
            }
            dbus_message_iter_close_container(dbus_iter, &array_iter);
            break;

        case QVariant::UInt:
            uint32_data = argument.toUInt();
            dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_UINT32, 
                                           &uint32_data);
            break;

        case QVariant::ULongLong:
            uint64_data = argument.toULongLong();
            dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_UINT64, 
                                           &uint64_data);
            break;

        case QVariant::LongLong:
            int64_data = argument.toLongLong();
            dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_INT64, 
                                           &int64_data);
            break;

        case QVariant::List:
            {
            QVariantList variantList = argument.toList();
            bool constantList = constantVariantList(variantList);
            DBusMessageIter array_iter;

            // List is mapped either as an DBUS array (all items same type)
            // DBUS struct (variable types) depending on constantList
            if (constantList) {
                // Resolve the signature for the first item
                QString signature = "";
                if (!variantList.isEmpty()) {
                    signature = variantToSignature(
                                variantList[0],
                                constantVariantList(variantList[0].toList()));
                }

                // Mapped as DBUS array
                dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_ARRAY,
                                                 signature.toAscii(),
                                                 &array_iter);

                foreach(QVariant listItem, variantList) {
                    appendVariantToDBusMessage(listItem, &array_iter);
                }

                dbus_message_iter_close_container(dbus_iter, &array_iter);
            } else {
                // Mapped as DBUS struct
                dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_STRUCT,
                                                 NULL,
                                                 &array_iter);

                foreach(QVariant listItem, variantList) {
                    appendVariantToDBusMessage(listItem, &array_iter);
                }

                dbus_message_iter_close_container(dbus_iter, &array_iter);
            }

            break;
            }
        default:
            qDebug() << "Unsupported variant type: " << argument.type();
            break;
    }

    return true;
}

static QVariant getVariantFromDBusMessage(DBusMessageIter *iter) {
    dbus_bool_t bool_data;
    dbus_int32_t int32_data;
    dbus_uint32_t uint32_data;
    dbus_int64_t int64_data;
    dbus_uint64_t uint64_data;
    char *str_data;
    char char_data;
    int argtype = dbus_message_iter_get_arg_type(iter);

    switch (argtype) {

        case DBUS_TYPE_BOOLEAN:
        {
            dbus_message_iter_get_basic(iter, &bool_data);
            QVariant variant((bool)bool_data);
            return variant;
        }

        case DBUS_TYPE_ARRAY:
        {
            // Handle all arrays here
            int elem_type = dbus_message_iter_get_element_type(iter);
            DBusMessageIter array_iter;

            dbus_message_iter_recurse(iter, &array_iter);

            if (elem_type == DBUS_TYPE_BYTE) {
                QByteArray byte_array;
                do {
                    dbus_message_iter_get_basic(&array_iter, &char_data);
                    byte_array.append(char_data);
                } while (dbus_message_iter_next(&array_iter));
                QVariant variant(byte_array);
                return variant;
            } else if (elem_type == DBUS_TYPE_STRING) {
                QStringList str_list;
                do {
                    dbus_message_iter_get_basic(&array_iter, &str_data);
                    str_list.append(str_data);
                } while (dbus_message_iter_next(&array_iter));
                QVariant variant(str_list);
                return variant;
            } else {
                QVariantList variantList;
                do {
                    variantList << getVariantFromDBusMessage(&array_iter);
                } while (dbus_message_iter_next(&array_iter));
                QVariant variant(variantList);
                return variant;
            }
            break;
        }

        case DBUS_TYPE_BYTE:
        {
            dbus_message_iter_get_basic(iter, &char_data);
            QChar ch(char_data);
            QVariant variant(ch);
            return variant;
        }

        case DBUS_TYPE_INT32:
        {
            dbus_message_iter_get_basic(iter, &int32_data);
            QVariant variant((int)int32_data);
            return variant;
        }

        case DBUS_TYPE_UINT32:
        {
            dbus_message_iter_get_basic(iter, &uint32_data);
            QVariant variant((uint)uint32_data);
            return variant;
        }

        case DBUS_TYPE_STRING:
        {
            dbus_message_iter_get_basic(iter, &str_data);
            QString str(QString::fromUtf8(str_data));
            QVariant variant(str);
            return variant;
        }

        case DBUS_TYPE_INT64:
        {
            dbus_message_iter_get_basic(iter, &int64_data);
            QVariant variant((qlonglong)int64_data);
            return variant;
        }

        case DBUS_TYPE_UINT64:
        {
            dbus_message_iter_get_basic(iter, &uint64_data);
            QVariant variant((qulonglong)uint64_data);
            return variant;
        }

        case DBUS_TYPE_STRUCT:
        {
            // Handle all structs here
            DBusMessageIter struct_iter;
            dbus_message_iter_recurse(iter, &struct_iter);

	    QVariantList variantList;
	    do {
	      variantList << getVariantFromDBusMessage(&struct_iter);
	    } while (dbus_message_iter_next(&struct_iter));
	    QVariant variant(variantList);
	    return variant;
        }

        default:
            qDebug() << "Unsupported DBUS type: " << argtype;
    }

    return QVariant();
}

static DBusHandlerResult signalHandler (DBusConnection *connection,
                                        DBusMessage *message,
                                        void *object_ref) {
    (void)connection;
    QString interface;
    QString signal;
    DBusDispatcher *dispatcher = (DBusDispatcher *)object_ref;

    if (dbus_message_get_type(message) == DBUS_MESSAGE_TYPE_SIGNAL) {
        interface = dbus_message_get_interface(message);
        signal = dbus_message_get_member(message);
        
        QList<QVariant> arglist;
        DBusMessageIter dbus_iter;

        if (dbus_message_iter_init(message, &dbus_iter)) {
            // Read return arguments
            while (dbus_message_iter_get_arg_type (&dbus_iter) != DBUS_TYPE_INVALID) {
                arglist << getVariantFromDBusMessage(&dbus_iter);
                dbus_message_iter_next(&dbus_iter);
            }
        }

        dispatcher->emitSignalReceived(interface, signal, arglist);
        return DBUS_HANDLER_RESULT_HANDLED;
    }
    (void)message;
    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

DBusDispatcher::DBusDispatcher(const QString& service,
                               const QString& path,
                               const QString& interface,
                               QObject *parent)
 : QObject(parent),
   d_ptr(new DBusDispatcherPrivate(service, path, interface, path)) {
    setupDBus();
}

DBusDispatcher::DBusDispatcher(const QString& service,
                               const QString& path,
                               const QString& interface,
                               const QString& signalPath,
                               QObject *parent)
 : QObject(parent),
   d_ptr(new DBusDispatcherPrivate(service, path, interface, signalPath)) {
    setupDBus();
}

DBusDispatcher::~DBusDispatcher()
{
    if (d_ptr->connection) {
        dbus_connection_close(d_ptr->connection);
        dbus_connection_unref(d_ptr->connection);
    }
    delete d_ptr;
}

void DBusDispatcher::setupDBus()
{
    d_ptr->connection = dbus_bus_get_private(DBUS_BUS_SYSTEM, NULL);

    if (d_ptr->connection == NULL)
        qDebug() << "Unable to get DBUS connection!";
    else {
        d_ptr->signal_vtable.message_function = signalHandler;

	dbus_connection_set_exit_on_disconnect(d_ptr->connection, FALSE);
        dbus_connection_setup_with_g_main(d_ptr->connection, g_main_context_get_thread_default());
        dbus_connection_register_object_path(d_ptr->connection, 
                                             d_ptr->signalPath.toLatin1(),
                                             &d_ptr->signal_vtable,
                                             this);
    }
}

static DBusMessage *prepareDBusCall(const QString& service,
                                    const QString& path,
                                    const QString& interface,
                                    const QString& method, 
                                    const QVariant& arg1 = QVariant(),
                                    const QVariant& arg2 = QVariant(),
                                    const QVariant& arg3 = QVariant(),
                                    const QVariant& arg4 = QVariant(),
                                    const QVariant& arg5 = QVariant(),
                                    const QVariant& arg6 = QVariant(),
                                    const QVariant& arg7 = QVariant(),
                                    const QVariant& arg8 = QVariant()) 
{
    DBusMessage *message = dbus_message_new_method_call(service.toLatin1(),
                                                        path.toLatin1(),
                                                        interface.toLatin1(),
                                                        method.toLatin1());
    DBusMessageIter dbus_iter;

    // Append variants to DBUS message
    QList<QVariant> arglist;
    if (arg1.isValid()) arglist << arg1;
    if (arg2.isValid()) arglist << arg2;
    if (arg3.isValid()) arglist << arg3;
    if (arg4.isValid()) arglist << arg4;
    if (arg5.isValid()) arglist << arg5;
    if (arg6.isValid()) arglist << arg6;
    if (arg7.isValid()) arglist << arg7;
    if (arg8.isValid()) arglist << arg8;

    dbus_message_iter_init_append (message, &dbus_iter);
    
    while (!arglist.isEmpty()) {
        QVariant argument = arglist.takeFirst();
        appendVariantToDBusMessage(argument, &dbus_iter);
    }

    return message;
}

QList<QVariant> DBusDispatcher::call(const QString& method, 
                                     const QVariant& arg1,
                                     const QVariant& arg2,
                                     const QVariant& arg3,
                                     const QVariant& arg4,
                                     const QVariant& arg5,
                                     const QVariant& arg6,
                                     const QVariant& arg7,
                                     const QVariant& arg8) {
    DBusMessageIter dbus_iter;
    DBusMessage *message = prepareDBusCall(d_ptr->service, d_ptr->path,
                                           d_ptr->interface, method,
                                           arg1, arg2, arg3, arg4, arg5,
                                           arg6, arg7, arg8);
    DBusMessage *reply = dbus_connection_send_with_reply_and_block(
                                                    d_ptr->connection,
                                                    message, -1, NULL);
    dbus_message_unref(message);

    QList<QVariant> replylist;
    if (reply != NULL &&  dbus_message_iter_init(reply, &dbus_iter)) {
        // Read return arguments
        while (dbus_message_iter_get_arg_type (&dbus_iter) != DBUS_TYPE_INVALID) {
            replylist << getVariantFromDBusMessage(&dbus_iter);
            dbus_message_iter_next(&dbus_iter);
        }
    }
    if (reply != NULL) dbus_message_unref(reply);
    return replylist;
}

class PendingCallInfo {
public:
    QString method;
    DBusDispatcher *dispatcher;
    DBusDispatcherPrivate *priv;
};

static void freePendingCallInfo(void *memory) {
    PendingCallInfo *info = (PendingCallInfo *)memory;
    delete info;
}

static void pendingCallFunction (DBusPendingCall *pending,
                                 void *memory) {
    PendingCallInfo *info = (PendingCallInfo *)memory;
    QString errorStr;
    QList<QVariant> replyList;
    DBusMessage *reply = dbus_pending_call_steal_reply (pending);

    Q_ASSERT(reply != NULL);

    if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
        errorStr = dbus_message_get_error_name (reply);
    } else {
        DBusMessageIter dbus_iter;
        dbus_message_iter_init(reply, &dbus_iter);
        // Read return arguments
        while (dbus_message_iter_get_arg_type (&dbus_iter) != DBUS_TYPE_INVALID) {
            replyList << getVariantFromDBusMessage(&dbus_iter);
            dbus_message_iter_next(&dbus_iter);
        }
    }

    info->priv->pending_calls.removeOne(pending);
    info->dispatcher->emitCallReply(info->method, replyList, errorStr);
    dbus_message_unref(reply);
    dbus_pending_call_unref(pending);
}

bool DBusDispatcher::callAsynchronous(const QString& method, 
                                      const QVariant& arg1,
                                      const QVariant& arg2,
                                      const QVariant& arg3,
                                      const QVariant& arg4,
                                      const QVariant& arg5,
                                      const QVariant& arg6,
                                      const QVariant& arg7,
                                      const QVariant& arg8) {
    DBusMessage *message = prepareDBusCall(d_ptr->service, d_ptr->path,
                                           d_ptr->interface, method,
                                           arg1, arg2, arg3, arg4, arg5,
                                           arg6, arg7, arg8);
    DBusPendingCall *call = NULL;
    dbus_bool_t ret = dbus_connection_send_with_reply(d_ptr->connection,
                                                      message, &call, -1);
    PendingCallInfo *info = new PendingCallInfo;
    info->method = method;
    info->dispatcher = this;
    info->priv = d_ptr;

    dbus_pending_call_set_notify(call, pendingCallFunction, info, freePendingCallInfo);
    d_ptr->pending_calls.append(call);
    return (bool)ret;
}

void DBusDispatcher::emitSignalReceived(const QString& interface, 
                                        const QString& signal,
                                        const QList<QVariant>& args) {
    emit signalReceived(interface, signal, args); }

void DBusDispatcher::emitCallReply(const QString& method,
                                   const QList<QVariant>& args,
                                   const QString& error) {
    emit callReply(method, args, error); }

void DBusDispatcher::synchronousDispatch(int timeout_ms)
{
    dbus_connection_read_write_dispatch(d_ptr->connection, timeout_ms);
}

} // Maemo namespace

