/*
 * Plasma applet to display indicators from libindicate
 *
 * Copyright 2009 Canonical Ltd.
 *
 * Authors:
 * - Aurélien Gâteau <aurelien.gateau@canonical.com>
 *
 * License: GPL v3
 */
// Self
#include "listenermodel.h"

// Qt
#include <QActionEvent>
#include <QDBusInterface>
#include <QEvent>
#include <QMenu>
#include <QRegExp>
#include <QTime>

// KDE
#include <KDebug>
#include <KDesktopFile>
#include <KIcon>
#include <KIconLoader>

// libindicate
#include <libindicate/indicator-messages.h>

// libindicate-qt
#include <qindicatedecode.h>

// libdbusmenu-qt
#include <dbusmenuimporter.h>

// Local

#define USE_ICONS_FOR_SERVERS

enum
{
    ActionItemType = QStandardItem::UserType,
    ServerItemType,
    IndicatorItemType
};

/**
 * Our DBusMenuImporter implementation
 */
class MyDBusMenuImporter : public DBusMenuImporter
{
public:
    MyDBusMenuImporter(const QString& service, const QString& path, QObject* parent = 0)
    : DBusMenuImporter(service, path, parent)
    {}

protected:
    virtual QIcon iconForName(const QString& name)
    {
        return KIcon(name);
    }
};


/**
 * An item representing an action from a DBusMenu, for indicate servers which
 * provide a "menu".
 */
class ActionItem : public QStandardItem
{
public:
    ActionItem(QAction* action)
    : mAction(action)
    {
        update();
    }

    void update()
    {
        setIcon(mAction->icon());
        setText(mAction->iconText());
    }

    QAction* action() const
    {
        return mAction;
    }

    int type() const
    {
        return ActionItemType;
    }

private:
    QAction* mAction;
};


/**
 * An item representing an indicate server.
 */
class ServerItem : public QStandardItem, protected QObject
{
public:
    ServerItem(QIndicate::Listener::Server* server)
    : mServer(server)
    , mMenuImporter(0)
    , mNextActionItemRow(0)
    {}

    void setMenuImporter(DBusMenuImporter* importer)
    {
        mMenuImporter = importer;
        mMenuImporter->setParent(this);
        mMenuImporter->menu()->installEventFilter(this);
    }

    int type() const
    {
        return ServerItemType;
    }

    QIndicate::Listener::Server* server() const
    {
        return mServer;
    }

protected:
    virtual bool eventFilter(QObject*, QEvent* event)
    {
        QActionEvent *actionEvent = 0;
        switch (event->type()) {
        case QEvent::ActionAdded:
        case QEvent::ActionChanged:
        case QEvent::ActionRemoved:
            actionEvent = static_cast<QActionEvent *>(event);
            break;
        default:
            return false;
        }
        switch (event->type()) {
        case QEvent::ActionAdded:
            addAction(actionEvent->action());
            break;
        case QEvent::ActionChanged:
            updateAction(actionEvent->action());
            break;
        case QEvent::ActionRemoved:
            removeAction(actionEvent->action());
            break;
        default:
            break;
        }
        return false;
    }

private:
    void addAction(QAction* action)
    {
        insertRow(mNextActionItemRow, new ActionItem(action));
        mNextActionItemRow++;
    }

    void updateAction(QAction* action)
    {
        ActionItem* item = findActionItemForAction(action);
        if (item) {
            item->update();
        } else {
            kWarning() << "No item for action" << action->text();
        }
    }

    void removeAction(QAction* action)
    {
        ActionItem* item = findActionItemForAction(action);
        if (!item) {
            kWarning() << "No item for action" << action->text();
            return;
        }
        removeRow(item->row());
        mNextActionItemRow--;
    }

    ActionItem* findActionItemForAction(QAction* action) const
    {
        for (int row=0; row < mNextActionItemRow; ++row) {
            ActionItem* item = static_cast<ActionItem*>(child(row));
            if (item->action() == action) {
                return item;
            }
        }
        return 0;
    }

public:
    QIndicate::Listener::Server* mServer;
    DBusMenuImporter* mMenuImporter;
    int mNextActionItemRow;
};


/**
 * An item representing an indicator of an indicate server
 */
class IndicatorItem : public QStandardItem
{
public:
    IndicatorItem(QIndicate::Listener::Indicator* indicator)
    : mIndicator(indicator)
    {}

    QIndicate::Listener::Indicator* indicator() const
    {
        return mIndicator;
    }

    int type() const
    {
        return IndicatorItemType;
    }

    QStringList outdatedKeyList() const
    {
        return mOutdatedKeyList;
    }

    void setOutdatedKeyList(const QStringList& list)
    {
        mOutdatedKeyList = list;
    }

    ServerItem* serverItem() const
    {
        return static_cast<ServerItem*>(parent());
    }

private:
    QIndicate::Listener::Indicator* mIndicator;
    QStringList mOutdatedKeyList;
};


typedef QPair<QIndicate::Listener::Server*, QIndicate::Listener::Indicator*> ServerIndicatorPair;
typedef QHash<ServerIndicatorPair, IndicatorItem*> ItemForIndicatorHash;
typedef QSet<QIndicate::Listener::Indicator*> IndicatorSet;
struct ListenerModelPrivate
{
    ListenerModel* q;

    QIndicate::Listener* mListener;
    QRegExp mAcceptedServerType;
    // Indicators we have received, but for which we haven't received a server
    // yet
    QHash<QIndicate::Listener::Server*, IndicatorSet> mWaitingIndicators;
    QHash<QIndicate::Listener::Server*, ServerItem*> mItemForServer;
    ItemForIndicatorHash mItemForIndicator;

    void updateIndicatorItem(IndicatorItem* indicatorItem)
    {
        ServerItem* serverItem = indicatorItem->serverItem();
        QIndicate::Listener::Server* server = serverItem->server();
        QIndicate::Listener::Indicator* indicator = indicatorItem->indicator();

        QStringList outdatedKeyList = indicatorItem->outdatedKeyList();
        indicatorItem->setOutdatedKeyList(QStringList());
        Q_FOREACH(const QString& key, outdatedKeyList) {
            mListener->getIndicatorPropertyAsVariant(server, indicator, key,
                                                     q, SLOT(slotPropertyReceived(
                                                             QIndicate::Listener::Server*,
                                                             QIndicate::Listener::Indicator*,
                                                             const QString&,
                                                             const QVariant&))
                                                     );
        }
    }

    void removeIndicatorItem(IndicatorItem* indicatorItem)
    {
        ServerItem* serverItem = indicatorItem->serverItem();
        QIndicate::Listener::Server* server = serverItem->server();
        QIndicate::Listener::Indicator* indicator = indicatorItem->indicator();
        mItemForIndicator.remove(ServerIndicatorPair(server, indicator));

        serverItem->removeRow(indicatorItem->row());
    }
};

ListenerModel::ListenerModel(QIndicate::Listener* listener, const QRegExp& acceptedServerType)
: d(new ListenerModelPrivate)
{
    d->q = this;
    d->mListener = listener;
    d->mAcceptedServerType = acceptedServerType;
    connect(d->mListener,
            SIGNAL(serverAdded(QIndicate::Listener::Server*, const QString&)),
            SLOT(slotServerAdded(QIndicate::Listener::Server*, const QString&))
            );
    connect(d->mListener,
            SIGNAL(serverRemoved(QIndicate::Listener::Server*, const QString&)),
            SLOT(slotServerRemoved(QIndicate::Listener::Server*))
            );
    connect(d->mListener,
            SIGNAL(serverCountChanged(QIndicate::Listener::Server*, int)),
            SLOT(slotServerCountChanged(QIndicate::Listener::Server*, int))
            );

    connect(d->mListener,
            SIGNAL(indicatorAdded(QIndicate::Listener::Server*,
                                  QIndicate::Listener::Indicator*)),
            SLOT(slotIndicatorAdded(QIndicate::Listener::Server*,
                                    QIndicate::Listener::Indicator*))
            );

    connect(d->mListener,
            SIGNAL(indicatorRemoved(QIndicate::Listener::Server*,
                                    QIndicate::Listener::Indicator*)),
            SLOT(slotIndicatorRemoved(QIndicate::Listener::Server*,
                                      QIndicate::Listener::Indicator*))
            );

    connect(d->mListener,
            SIGNAL(indicatorModified(QIndicate::Listener::Server*,
                                     QIndicate::Listener::Indicator*,
                                     const QString&)),
            SLOT(slotIndicatorModified(QIndicate::Listener::Server*,
                                     QIndicate::Listener::Indicator*,
                                     const QString&))
            );
}

ListenerModel::~ListenerModel()
{
    delete d;
}

void ListenerModel::slotServerAdded(QIndicate::Listener::Server* server, const QString& type)
{
    if (d->mAcceptedServerType.indexIn(type) == -1) {
        d->mWaitingIndicators.remove(server);
        return;
    }

    if (d->mItemForServer.contains(server)) {
        kWarning() << "We already know about server" << server;
        return;
    }

    ServerItem* item = new ServerItem(server);
    item->setData(type, ServerTypeRole);
    d->mItemForServer.insert(server, item);
    appendRow(item);

    // Simulate slotIndicatorAdded for waiting indicators
    IndicatorSet set = d->mWaitingIndicators.take(server);
    Q_FOREACH(QIndicate::Listener::Indicator* indicator, set) {
        slotIndicatorAdded(server, indicator);
    }

    d->mListener->getServerDesktopFile(server,
                                       this,
                                       SLOT(slotDesktopFileReceived(
                                            QIndicate::Listener::Server*,
                                            const QByteArray&)
                                            )
                                       );
}


void ListenerModel::slotDesktopFileReceived(QIndicate::Listener::Server* server, const QByteArray& _fileName)
{
    QString fileName = QIndicate::Decode::stringFromValue(_fileName);
    KDesktopFile desktopFile(fileName);
    QString name = desktopFile.readName();
    if (name.isEmpty()) {
        name = fileName.section('/', -1);
    }

    QStandardItem* item = d->mItemForServer.value(server);
    Q_ASSERT(item);
    item->setText(name);

#ifdef USE_ICONS_FOR_SERVERS
    QPixmap icon = KIconLoader::global()->loadIcon(desktopFile.readIcon(),
                                                   KIconLoader::Small,
                                                   0 /* size */,
                                                   KIconLoader::DefaultState,
                                                   QStringList() /* overlays */,
                                                   0L /* path_store */,
                                                   true /* canReturnNull */);
    if (!icon.isNull()) {
        item->setData(QVariant(icon), Qt::DecorationRole);
    }
#endif

    d->mListener->getServerMenuObjectPath(server,
                                          this,
                                          SLOT(slotMenuObjectPathReceived(
                                               QIndicate::Listener::Server*,
                                               const QString&)
                                               )
                                          );
}

void ListenerModel::slotMenuObjectPathReceived(QIndicate::Listener::Server* server, const QString& objectPath)
{
    if (objectPath.isEmpty()) {
        kWarning() << "Empty objectPath!";
        return;
    }

    ServerItem* serverItem = d->mItemForServer.value(server);
    if (!serverItem) {
        kWarning() << "No server info found for server" << server;
        return;
    }

    QString dbusName = d->mListener->getServerMenuDBusName(server);
    if (dbusName.isEmpty()) {
        kWarning() << "Empty dbusname!";
        return;
    }

    DBusMenuImporter* importer = new MyDBusMenuImporter(dbusName, objectPath);
    serverItem->setMenuImporter(importer);
}

void ListenerModel::slotServerRemoved(QIndicate::Listener::Server* server)
{
    if (d->mWaitingIndicators.contains(server)) {
        d->mWaitingIndicators.remove(server);
        return;
    }
    ServerItem* serverItem = d->mItemForServer.value(server);
    if (!serverItem) {
        kWarning() << "No item found for server" << server;
        return;
    }

    // Remove all children for this server
    for (int row = serverItem->rowCount() - 1; row >=0; --row) {
        QStandardItem* child = serverItem->child(row);
        if (child->type() == IndicatorItemType) {
            d->removeIndicatorItem(static_cast<IndicatorItem*>(child));
        }
        // Nothing to do for ActionItemType children: removing the serverItem
        // will do the work
    }

    // Delete server item
    d->mItemForServer.remove(server);
    removeRow(serverItem->row());
}

void ListenerModel::slotServerCountChanged(QIndicate::Listener::Server* server, int count)
{
    QStandardItem* item = d->mItemForServer.value(server);
    if (!item) {
        kWarning() << "No item found for server" << server;
        return;
    }

    item->setData(QVariant(count), CountRole);
}

void ListenerModel::slotIndicatorAdded(QIndicate::Listener::Server* server,
                                       QIndicate::Listener::Indicator* indicator)
{
    static QStringList outdatedKeyList = QStringList()
        << INDICATE_INDICATOR_MESSAGES_PROP_NAME
        << INDICATE_INDICATOR_MESSAGES_PROP_ICON
        << INDICATE_INDICATOR_MESSAGES_PROP_TIME
        << INDICATE_INDICATOR_MESSAGES_PROP_ATTENTION
        << INDICATE_INDICATOR_MESSAGES_PROP_COUNT;

    QStandardItem* serverItem = d->mItemForServer.value(server);
    if (!serverItem) {
        kWarning() << "We received indicatorAdded() signal before serverAdded() signal!";
        d->mWaitingIndicators[server] << indicator;
        // We will be called again when the server is ready
        return;
    }

    IndicatorItem* indicatorItem = new IndicatorItem(indicator);
    indicatorItem->setOutdatedKeyList(outdatedKeyList);
    d->mItemForIndicator.insert(ServerIndicatorPair(server, indicator), indicatorItem);
    serverItem->appendRow(indicatorItem);

    d->updateIndicatorItem(indicatorItem);
}

void ListenerModel::slotIndicatorModified(QIndicate::Listener::Server* server,
                                          QIndicate::Listener::Indicator* indicator,
                                          const QString& key)
{
    IndicatorItem* item = d->mItemForIndicator.value(ServerIndicatorPair(server, indicator));
    if (!item) {
        if (!d->mWaitingIndicators.contains(server)) {
            kError() << "Unknown indicator" << indicator;
        }
        return;
    }

    QStringList outdatedKeyList = item->outdatedKeyList();
    if (outdatedKeyList.contains(key)) {
        return;
    }
    outdatedKeyList << key;
    item->setOutdatedKeyList(outdatedKeyList);
    d->updateIndicatorItem(item);
}

void ListenerModel::slotIndicatorRemoved(QIndicate::Listener::Server* server,
                                         QIndicate::Listener::Indicator* indicator)
{
    if (d->mWaitingIndicators.contains(server)) {
        d->mWaitingIndicators[server].remove(indicator);
        return;
    }
    IndicatorItem* item = d->mItemForIndicator.value(ServerIndicatorPair(server, indicator));
    if (!item) {
        kWarning() << "No item for indicator" << indicator;
        return;
    }

    QStandardItem* serverItem = item->parent();
    if (!serverItem) {
        kError() << "Item for indicator" << indicator << "has no parent!";
        return;
    }
    d->removeIndicatorItem(item);
}

void ListenerModel::getProxiesForIndex(const QModelIndex& index,
                                       QIndicate::Listener::Server** server,
                                       QIndicate::Listener::Indicator** indicator) const
{
    Q_ASSERT(server);
    Q_ASSERT(indicator);
    *server = 0;
    *indicator = 0;
    if (!index.isValid()) {
        return;
    }

    QStandardItem* item = itemFromIndex(index);
    switch (item->type()) {
    case IndicatorItemType:
        *indicator = static_cast<IndicatorItem*>(item)->indicator();
        *server = static_cast<ServerItem*>(item->parent())->server();
        break;
    case ServerItemType:
        *server = static_cast<ServerItem*>(item)->server();
        break;
    case ActionItemType:
        *server = static_cast<ServerItem*>(item->parent())->server();
        break;
    }
}

void ListenerModel::slotPropertyReceived(QIndicate::Listener::Server* server,
                                         QIndicate::Listener::Indicator* indicator,
                                         const QString& key,
                                         const QVariant& value)
{
    QStandardItem* item = d->mItemForIndicator.value(ServerIndicatorPair(server, indicator));
    if (!item) {
        kWarning() << "No item for indicator" << indicator;
        return;
    }

    if (key == INDICATE_INDICATOR_MESSAGES_PROP_NAME) {
        QByteArray data = value.toByteArray();
        item->setText(QIndicate::Decode::stringFromValue(data));
    } else if (key == INDICATE_INDICATOR_MESSAGES_PROP_ICON) {
        QByteArray data = value.toByteArray();
        QImage image = QIndicate::Decode::imageFromValue(data);
        QPixmap pix = QPixmap::fromImage(image);
        item->setData(KIcon(pix), Qt::DecorationRole);
    } else if (key == INDICATE_INDICATOR_MESSAGES_PROP_TIME) {
        QByteArray data = value.toByteArray();
        QDateTime dateTime = QIndicate::Decode::dateTimeFromValue(data);
        item->setData(QVariant(dateTime), IndicatorDateTimeRole);
    } else if (key == INDICATE_INDICATOR_MESSAGES_PROP_ATTENTION) {
        QVariant old = item->data(IndicatorDrawAttentionRole);
        item->setData(value, IndicatorDrawAttentionRole);
        if (old != value) {
            emit drawAttentionChanged(indexFromItem(item));
        }
    } else if (key == INDICATE_INDICATOR_MESSAGES_PROP_COUNT) {
        item->setData(value, CountRole);
    } else {
        kWarning() << "Unhandled key" << key;
    }
}

void ListenerModel::activate(const QModelIndex& index)
{
    QStandardItem* item = itemFromIndex(index);
    if (item->type() == ActionItemType) {
        static_cast<ActionItem*>(item)->action()->trigger();
        return;
    }

    QIndicate::Listener::Server* server = 0;
    QIndicate::Listener::Indicator* indicator = 0;

    getProxiesForIndex(index, &server, &indicator);
    d->mListener->display(server, indicator);

}

#include "listenermodel.moc"
