/*
 * Copyright (C) 2006  Kevin Smith
 * Copyright (C) 2008  Maciej Niedzielski
 */

#include "pluginmanager.h"

#include "applicationinfo.h"
#include "avatars.h"
#include "chatdlg.h"
#include "eventfilter.h"
#include "groupchatdlg.h"
#include "iqfilter.h"
#include "iqnamespacefilter.h"
#include "iris/xmpp_caps.h"
#include "iris/xmpp_client.h"
#include "iris/xmpp_message.h"
#include "iris/xmpp_task.h"
#include "messageview.h"
#include "optionaccessor.h"
#include "optionsdlg.h"
#include "pluginhost.h"
#include "popupmanager.h"
#include "psiaccount.h"
#include "psicon.h"
#include "psicontact.h"
#include "psiiconset.h"
#include "psimediaprovider.h"
#include "psioptions.h"
#include "psiplugin.h"
#include "stanzafilter.h"
#include "stanzasender.h"

#include <QLabel>
#include <QMetaObject>
#include <QPluginLoader>
#include <QtCore>
#include <QtCrypto>

// TODO(mck)
// - use native separators when displaying file path

/**
 * Helper class used to process incoming XML in plugins.
 * This task should work as long as the related account exists,
 * that's why we override onDisconnect (called when XML stream ends)
 * to prevent it from stopping prematurely.
 *
 * According to common sense, tasks should have at least
 * a vaguely defined execution time, however, this one runs
 * indefinitely long and feels more like a hook/handler.
 * Therefore it should probably be refactored to one.
 */
class PluginManager::StreamWatcher : public XMPP::Task {
public:
    StreamWatcher(Task *t, PluginManager *m, int a) : Task(t), manager(m), account(a) { }
    bool take(const QDomElement &e) { return manager->incomingXml(account, e); }
    void onDisconnect()
    {
        // never finish on client disconnect
    }
    PluginManager *manager;
    int            account;
};

/**
 * Function to obtain all the directories in which plugins can be stored
 * \return List of plugin directories
 */
static QStringList pluginDirs() { return ApplicationInfo::pluginDirs(); }

/**
 * Method for accessing the singleton instance of the class.
 * Instanciates if no instance yet exists.
 * \return Pointer to the plugin manager.
 */
PluginManager *PluginManager::instance()
{
    if (!instance_) {
        instance_ = new PluginManager();
    }
    return instance_;
}

/**
 * Default constructor. Locates all plugins, sets watchers on those directories to
 * locate new ones and loads those enabled in the config.
 */
PluginManager::PluginManager() : QObject(nullptr), psi_(nullptr)
{
    updatePluginsList();
    auto const &dirs = pluginDirs();
    for (const QString &path : dirs) {
        QCA::DirWatch *dw = new QCA::DirWatch(path, this);
        connect(dw, SIGNAL(changed()), SLOT(dirsChanged()));
        dirWatchers_.append(dw);
    }

    _messageViewJSFiltersTimer = new QTimer(this);
    _messageViewJSFiltersTimer->setSingleShot(true);
    _messageViewJSFiltersTimer->setInterval(10); // to be able to restart in case of batch events
    connect(_messageViewJSFiltersTimer, &QTimer::timeout, this, &PluginManager::jsFiltersUpdated);

    connect(PsiOptions::instance(), &PsiOptions::optionChanged, this, &PluginManager::optionChanged);
}

void PluginManager::initNewSession(PsiCon *psi)
{
    psi_ = psi;
    clients_.clear();
    accountIds_.clear();
    loadEnabledPlugins();
}

/**
 * Updates list of known plugins by reading all plugin directories
 * and returns the list of new plugins
 */
QList<PluginHost *> PluginManager::updatePluginsList()
{
    QList<PluginHost *> newPlugins;

    auto const &dirs = pluginDirs();
    for (const QString &d : dirs) {
        QDir        dir(d);
        auto const &entries = dir.entryInfoList(QDir::Files);
        for (const QFileInfo &fileInfo : entries) {
            QString file = fileInfo.canonicalFilePath();
            if (QLibrary::isLibrary(file)) {
#ifndef PLUGINS_NO_DEBUG
                qDebug("Found plugin: %s", qPrintable(file));
#endif
                if (!pluginByFile_.contains(file)) {
                    PluginHost *host = new PluginHost(this, file);
                    connect(host, &PluginHost::enabled, this,
                            [this, shortName = host->shortName()]() { emit pluginEnabled(shortName); });
                    connect(host, &PluginHost::disabled, this,
                            [this, shortName = host->shortName()]() { emit pluginDisabled(shortName); });
                    if (host->isValid() && !hosts_.contains(host->shortName())) {
                        hosts_[host->shortName()] = host;
                        pluginByFile_[file]       = host;
                        newPlugins.append(host);
                        if (host->priority() == PsiPlugin::PriorityHighest || !pluginsByPriority_.size()) {
                            pluginsByPriority_.push_front(host);
                        } else {
                            // find position for insertion
                            int i = pluginsByPriority_.size();
                            while (i && host->priority() > pluginsByPriority_[i - 1]->priority()) {
                                i--;
                            }
                            pluginsByPriority_.insert(i, host);
                        }
                    }
                } else {
#ifndef PLUGINS_NO_DEBUG
                    qDebug("Which we already knew about");
#endif
                }
            }
        }
    }

    return newPlugins;
}

/**
 * This slot is executed when the contents of a plugin directory changes
 * It causes the available plugin list to be refreshed.
 */
void PluginManager::dirsChanged()
{
    auto const &pl = updatePluginsList();
    for (PluginHost *plugin : pl) {
        loadPluginIfEnabled(plugin);
    }
}

void PluginManager::accountDestroyed()
{
    PsiAccount *pa = static_cast<PsiAccount *>(sender());
    accountIds_.removeAccount(pa);
}

/**
 * This causes all plugins that are both set for auto-load, and available
 * to be loaded.
 */
void PluginManager::loadEnabledPlugins()
{
#ifndef PLUGINS_NO_DEBUG
    qDebug("Loading enabled plugins");
#endif
    for (PluginHost *plugin : std::as_const(pluginsByPriority_)) {
        loadPluginIfEnabled(plugin);
    }
}

void PluginManager::loadPluginIfEnabled(PluginHost *plugin)
{
    const QString option = QString("%1.%2").arg(loadOptionPrefix, plugin->shortName());
    QVariant      load   = PsiOptions::instance()->getOption(option, false);
    if (load.toBool()) {
#ifndef PLUGINS_NO_DEBUG
        qDebug("Plugin %s is enabled in config: loading", qPrintable(plugin->shortName()));
#endif
        plugin->enable();
    }
}

/**
 * Called when an option changes to load or unload a plugin if it's a plugin
 * option
 * \param option Option changed
 */
void PluginManager::optionChanged(const QString &option)
{
    bool          pluginOpt = option.startsWith(loadOptionPrefix);
    const QString shortName = option.split(".").last();
    for (PluginHost *plugin : std::as_const(pluginByFile_)) {
        plugin->optionChanged(option);
        if (pluginOpt && plugin->shortName() == shortName) {
            bool shouldUpdateFeatures;
            if (PsiOptions::instance()->getOption(option).toBool()) {
                plugin->enable();
                shouldUpdateFeatures = !plugin->pluginFeatures().isEmpty();
            } else {
                shouldUpdateFeatures = !plugin->pluginFeatures().isEmpty();
                if (optionsWidget_)
                    delete optionsWidget_;
                plugin->unload();
            }
            if (shouldUpdateFeatures) {
                updateFeatures();
            }
        }
    }
}

void PluginManager::updateFeatures()
{
    for (int acc_id = 0; accountIds_.isValidRange(acc_id); ++acc_id) {
        PsiAccount *pa = accountIds_.account(acc_id);
        pa->updateFeatures();
        if (pa->isConnected()) {
            pa->setStatusActual(pa->status());
        }
    }
}

/**
 * Loads all available plugins
 */
void PluginManager::loadAllPlugins()
{
#ifndef PLUGINS_NO_DEBUG
    qDebug("Loading all plugins");
#endif
    // Any static (compiled in) plugins we happen to have
    /*foreach( QObject* plugin, QPluginLoader::staticInstances() ) {
        loadPlugin( plugin );
    }*/

    // Now look for external plugins
    for (PluginHost *plugin : std::as_const(hosts_)) {
        plugin->load();
        if (plugin->enable())
            emit pluginEnabled(plugin->shortName());
    }
}

/**
 * Unloads all Psi plugins.
 * \return Success of unloading all plugins; if any plugins couldn't be
 *         unloaded, false.
 */
bool PluginManager::unloadAllPlugins()
{
#ifndef PLUGINS_NO_DEBUG
    qDebug("Unloading all plugins");
#endif
    bool ok = true;
    for (PluginHost *plugin : std::as_const(hosts_)) {
        if (!plugin->unload()) {
            ok = false;
        }
    }
    return ok;
}

bool PluginManager::isAvailable(const QString &plugin) const { return hosts_.contains(plugin); }

bool PluginManager::isEnabled(const QString &plugin) const
{
    bool enabled = false;
    if (hosts_.contains(plugin)) {
        enabled = hosts_[plugin]->isEnabled();
    }
    return enabled;
}

/**
 * Find the file which provides the named plugin. If the named plugin is not
 * known, an empty string is provided.
 * \param plugin Name of the plugin.
 * \return Path to the plugin file.
 */
QString PluginManager::pathToPlugin(const QString &plugin) const
{
    QString path;
    if (hosts_.contains(plugin)) {
        path = hosts_[plugin]->path();
    }
    return path;
}

/**
 * Returns short name of the named plugin. If the named plugin is not
 * known, an empty string is provided.
 * \param plugin Name of the plugin.
 * \return Path to the plugin file.
 */
// QString PluginManager::shortName(const QString &plugin) const
//{
//    QString name;
//    if (hosts_.contains(plugin)) {
//        name = hosts_[plugin]->shortName();
//    }
//    return name;
//}

QString PluginManager::pluginName(const QString &shortName) const
{
    auto host = hosts_.value(shortName);
    return host ? host->name() : QString();
}

QString PluginManager::version(const QString &plugin) const
{
    QString name;
    if (hosts_.contains(plugin)) {
        name = hosts_[plugin]->version();
    }
    return name;
}

QString PluginManager::vendor(const QString &plugin) const
{
    auto it = hosts_.find(plugin);
    return it == hosts_.end() ? QString() : it.value()->vendor();
}

QString PluginManager::description(const QString &plugin) const
{
    auto it = hosts_.find(plugin);
    return it == hosts_.end() ? QString() : it.value()->description();
}

/**
 * Returns a list of available plugin names found in all plugin directories.
 */
QStringList PluginManager::availablePlugins() const { return hosts_.keys(); }

/**
 * Provides a pointer to a QWidget providing the options dialog for the
 * named plugin, if it exists, else nullptr.
 * \param plugin Name of the plugin.
 * \return Pointer to the options widget for the named plugin.
 */
QWidget *PluginManager::optionsWidget(const QString &plugin)
{
    if (optionsWidget_) {
        delete optionsWidget_;
    }

    if (hosts_.contains(plugin)) {
        optionsWidget_ = hosts_[plugin]->optionsWidget();
    }

    if (!optionsWidget_) {
#ifndef PLUGINS_NO_DEBUG
        qWarning("Attempting to get options for %s which doesn't exist", qPrintable(plugin));
#endif
        optionsWidget_ = new QLabel(tr("This plugin has no user configurable options"));
    }
    return optionsWidget_;
}

/**
 * \brief Give each plugin the opportunity to set shortcuts
 *
 * Each plugin can set its own global shortcuts
 */
void PluginManager::setShortcuts()
{
    for (PluginHost *host : std::as_const(pluginByFile_)) {
        host->setShortcuts();
    }
}

/**
 * \brief Give each plugin the opportunity to process the incoming message event
 *
 * Each plugin is passed the event in turn. Any plugin may then modify the event
 * and may cause the event to be silently discarded.
 *
 * \param account Pointer to the PsiAccount responsible
 * \param event Incoming event
 * \return Continue processing the event; true if the event should be silently discarded.
 */
bool PluginManager::processMessage(PsiAccount *account, const QString &jidFrom, const QString &body,
                                   const QString &subject)
{
    bool handled = false;
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        if (host->processMessage(accountIds_.id(account), jidFrom, body, subject)) {
            handled = true;
            break;
        }
    }
    return handled;
}

/**
 * \brief Give each plugin the opportunity to process the incoming event
 *
 * Each plugin is passed the event in turn. Any plugin may then modify the event
 * and may cause the event to be silently discarded.
 *
 * \param account Pointer to the PsiAccount responsible
 * \param event Incoming event
 * \return Continue processing the event; true if the event should be silently discarded.
 */
bool PluginManager::processEvent(PsiAccount *account, QDomElement &event)
{
    bool      handled = false;
    const int acc_id  = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        if (host->processEvent(acc_id, event)) {
            handled = true;
            break;
        }
    }
    return handled;
}

/**
 * process an outgoing message
 */
bool PluginManager::processOutgoingMessage(PsiAccount *account, const QString &jidTo, QString &body,
                                           const QString &type, QString &subject)
{
    bool      handled = false;
    const int acc_id  = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginByFile_)) {
        if (host->processOutgoingMessage(acc_id, jidTo, body, type, subject)) {
            handled = true;
            break;
        }
    }
    return handled;
}

void PluginManager::processOutgoingStanza(PsiAccount *account, QDomElement &stanza)
{
    const int acc_id = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginByFile_)) {
        if (host->outgoingXml(acc_id, stanza)) {
            break;
        }
    }
}

/**
 * Notify to plugins that an account is going to connect.
 */
void PluginManager::startLogin(PsiAccount *account)
{
    const int acc_id = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginByFile_)) {
        host->logout(acc_id);
        emit accountBeforeLogin(acc_id);
    }
}

/**
 * Notify to plugins that an account will go offline now.
 */
void PluginManager::logout(PsiAccount *account)
{
    const int acc_id = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginByFile_)) {
        host->logout(acc_id);
        emit accountLoggedOut(acc_id);
    }
}

void PluginManager::addSettingPage(OAH_PluginOptionsTab *tab)
{
    settingsTabs_.append(tab);
    OptionsDlg *w = qobject_cast<OptionsDlg *>(psi_->dialogFind("OptionsDlg"));
    if (w) {
        w->addPluginWrapperTab(tab);
    }
}

void PluginManager::removeSettingPage(OAH_PluginOptionsTab *tab)
{
    OptionsDlg *w = qobject_cast<OptionsDlg *>(psi_->dialogFind("OptionsDlg"));
    if (w) {
        w->removeTab(tab->id());
    }
    settingsTabs_.removeOne(tab);
}

QList<OAH_PluginOptionsTab *> PluginManager::settingsPages() const { return settingsTabs_; }

/**
 * \brief Give each plugin the opportunity to process the incoming xml
 *
 * Each plugin is passed the xml in turn using various filter interfaces
 * (for example, see StanzaFilter or IqFilter).
 * Any plugin may then modify the xml and may cause the stanza to be
 * silently discarded.
 *
 * \param account Identifier of the PsiAccount responsible
 * \param xml Incoming XML
 * \return Continue processing the event; true if the event should be silently discarded.
 */
bool PluginManager::incomingXml(int account, const QDomElement &xml)
{
    bool handled = false;
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        if (host->incomingXml(account, xml)) {
            handled = true;
            break;
        }
    }
    return handled;
}

/**
 * Called by PluginHost when its hosted plugin wants to send xml stanza.
 *
 * \param account Identifier of the PsiAccount responsible
 * \param xml XML stanza to be sent
 */
void PluginManager::sendXml(int account, const QString &xml)
{
    // TODO(mck)
    // - think if it is better to ask plugin(host) for string or xml node
    // - validate things
    // - add id if missing
    // - maybe use appropriate Task to send
    // - make psi aware of things that are being send
    //   (for example, pipeline messages through history system)

    if (account < clients_.size()) {
        clients_[account]->send(xml);
    }
}

/**
 * Returns unique stanza identifier in account's stream
 *
 * \param account Identifier of the PsiAccount responsible
 * \return Unique ID to be used for when sending a stanza
 */
QString PluginManager::uniqueId(int account) const
{
    QString id;
    if (account < clients_.size()) {
        id = clients_[account]->genUniqueId();
    }
    return id;
}

QString PluginManager::getStatus(int account) const
{
    Status      S;
    PsiAccount *pa = accountIds_.account(account);
    if (pa)
        S = pa->status();
    return S.typeString();
}

QString PluginManager::getStatusMessage(int account) const
{
    Status      S;
    PsiAccount *pa = accountIds_.account(account);
    if (pa)
        S = pa->status();
    return S.status();
}

static inline const QString getProxyId(PsiAccount *pa) { return pa->accountOptions().proxyID; }

QString PluginManager::proxyHost(int account) const
{
    QString     host;
    PsiAccount *pa = accountIds_.account(account);
    if (pa)
        host = ProxyManager::instance()->getItem(getProxyId(pa)).settings.host;
    return host;
}

int PluginManager::proxyPort(int account) const
{
    int         port = -1;
    PsiAccount *pa   = accountIds_.account(account);
    if (pa)
        port = ProxyManager::instance()->getItem(getProxyId(pa)).settings.port;
    return port;
}

QString PluginManager::proxyUser(int account) const
{
    QString     user;
    PsiAccount *pa = accountIds_.account(account);
    if (pa)
        user = ProxyManager::instance()->getItem(getProxyId(pa)).settings.user;
    return user;
}

QString PluginManager::proxyPassword(int account) const
{
    QString     pass;
    PsiAccount *pa = accountIds_.account(account);
    if (pa)
        pass = ProxyManager::instance()->getItem(getProxyId(pa)).settings.pass;
    return pass;
}

QStringList PluginManager::getRoster(int account) const
{
    QStringList list;
    list << "-1";
    if (accountIds_.isValidRange(account)) {
        list.clear();
        PsiAccount *pa = accountIds_.account(account);
        if (pa) {
            QList<PsiContact *> roster = pa->contactList();
            for (int i = 0; i < roster.size(); i++) {
                list.push_back(roster.at(i)->jid().bare());
            }
        }
    }
    return list;
}

QString PluginManager::getJid(int account) const
{
    QString Jid = "-1";
    if (accountIds_.isValidRange(account)) {
        Jid.clear();
        PsiAccount *pa = accountIds_.account(account);
        if (pa)
            Jid = pa->jid().bare();
    }
    return Jid;
}

QString PluginManager::getId(int account) const
{
    QString id = "-1";
    if (accountIds_.isValidRange(account)) {
        id.clear();
        PsiAccount *pa = accountIds_.account(account);
        if (pa)
            id = pa->id();
    }
    return id;
}

QString PluginManager::getName(int account) const
{
    QString     nm;
    PsiAccount *pa = accountIds_.account(account);
    if (pa)
        nm = pa->name();
    return nm;
}

QString PluginManager::getPgpKey(int account) const
{
    QString     keyId;
    PsiAccount *pa = accountIds_.account(account);
    if (pa)
        keyId = pa->pgpKeyId();
    return keyId;
}

QMap<QString, QString> PluginManager::getKnownPgpKeys(int account) const
{
    QMap<QString, QString> out;
    PsiAccount            *pa = accountIds_.account(account);
    if (pa) {
        UserAccount acc = pa->userAccount();
        for (const auto &item : std::as_const(acc.pgpKnownKeys)) {
            out[item.key()] = item.data();
        }
    }
    return out;
}

int PluginManager::findOnlineAccountForContact(const QString &jid) const
{
    Jid j(jid);
    for (int acc_id = 0; accountIds_.isValidRange(acc_id); ++acc_id) {
        PsiAccount *pa = accountIds_.account(acc_id);
        if (pa && pa->isAvailable() && pa->findContact(j))
            return acc_id;
    }
    return -1;
}

bool PluginManager::setActivity(int account, const QString &jid, QDomElement xml)
{
    PsiAccount *pa = accountIds_.account(account);
    if (!pa)
        return false;

    XMPP::Jid     userJid(jid);
    UserListItem *item = pa->userList()->find(userJid);

    if (!item)
        return false;

    Activity act = Activity();
    if (!xml.isNull())
        act = Activity(xml);
    item->setActivity(act);
    return true;
}

bool PluginManager::setMood(int account, const QString &jid, QDomElement xml)
{
    PsiAccount *pa = accountIds_.account(account);
    if (!pa)
        return false;

    XMPP::Jid     userJid(jid);
    UserListItem *item = pa->userList()->find(userJid);

    if (!item)
        return false;

    Mood mood = Mood();
    if (!xml.isNull())
        mood = Mood(xml);
    item->setMood(mood);
    return true;
}

bool PluginManager::setTune(int account, const QString &jid, const QString &tune)
{
    PsiAccount *pa = accountIds_.account(account);
    if (!pa)
        return false;

    XMPP::Jid     userJid(jid);
    UserListItem *item = pa->userList()->find(userJid);

    if (!item)
        return false;

    item->setTune(tune);
    return true;
}

void PluginManager::initPopup(const QString &text, const QString &title, const QString &icon, int type)
{
    const PsiIcon *ico = IconsetFactory::iconPtr(icon);
    psi_->popupManager()->doPopup(nullptr, Jid(), ico, title, QPixmap(), nullptr, text, true,
                                  PopupManager::PopupType(type));
}

void PluginManager::initPopupForJid(int account, const QString &jid, const QString &text, const QString &title,
                                    const QString &icon, int type)
{
    XMPP::Jid      j(jid);
    const PsiIcon *ico = IconsetFactory::iconPtr(icon);
    PsiAccount    *pa  = accountIds_.account(account);
    if (pa) {
        UserListItem *i         = pa->findFirstRelevant(j);
        PsiIcon      *statusIco = PsiIconset::instance()->statusPtr(i);
        const QPixmap pix       = pa->avatarFactory()->getAvatar(j);
        psi_->popupManager()->doPopup(pa, j, ico, title, pix, statusIco, text, true, PopupManager::PopupType(type));
        return;
    }
    psi_->popupManager()->doPopup(nullptr, Jid(), ico, title, QPixmap(), nullptr, text, true,
                                  PopupManager::PopupType(type));
}

int PluginManager::registerOption(const QString &name, int initValue, const QString &path)
{
    return psi_->popupManager()->registerOption(name, initValue, path);
}

void PluginManager::unregisterOption(const QString &name) { psi_->popupManager()->unregisterOption(name); }

int PluginManager::popupDuration(const QString &name) const { return psi_->popupManager()->value(name); }

void PluginManager::setPopupDuration(const QString &name, int value) { psi_->popupManager()->setValue(name, value); }

void PluginManager::addAccountMenu(QMenu *menu, PsiAccount *account)
{
    int i = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        host->addAccountMenu(menu, i);
    }
}

void PluginManager::addContactMenu(QMenu *menu, PsiAccount *account, QString jid)
{
    int i = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        host->addContactMenu(menu, i, jid);
    }
}

void PluginManager::setupChatTab(QWidget *tab, PsiAccount *account, const QString &contact)
{
    int i = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        host->setupChatTab(tab, i, contact);
    }
}

void PluginManager::setupGCTab(QWidget *tab, PsiAccount *account, const QString &contact)
{
    int i = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        host->setupGCTab(tab, i, contact);
    }
}

bool PluginManager::appendingChatMessage(PsiAccount *account, const QString &contact, QString &body, QDomElement &html,
                                         bool local)
{
    bool handled = false;
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        if (host->appendingChatMessage(accountIds_.id(account), contact, body, html, local)) {
            handled = true;
            break;
        }
    }
    return handled;
}

bool PluginManager::hasInfoProvider(const QString &plugin) const
{
    if (hosts_.contains(plugin))
        return hosts_[plugin]->hasInfoProvider();

    return false;
}

QString PluginManager::pluginInfo(const QString &plugin) const
{
    auto it = hosts_.find(plugin);
    if (it == hosts_.end())
        return QString();
    QString info = it.value()->pluginInfo();
    if (info.isEmpty())
        info = it.value()->description();
    return info;
}

QIcon PluginManager::icon(const QString &plugin) const
{
    QIcon icon;
    if (hosts_.contains(plugin)) {
        icon = hosts_[plugin]->icon();
    }

    return icon;
}

QStringList PluginManager::pluginFeatures() const
{
    QStringList features;
    for (PluginHost *host : pluginByFile_) {
        features << host->pluginFeatures();
    }
    return features;
}

/**
 * Tells the plugin manager about an XMPP::Client and the owning PsiAccount
 */
void PluginManager::addAccount(PsiAccount *account, XMPP::Client *client)
{
    clients_.append(client);
    const int id = accountIds_.appendAccount(account);
    new StreamWatcher(client->rootTask(), this, id); // this StreamWatcher instance isn't stored anywhere
    // and probably leaks (if go(true) isn't called somewhere else)
    connect(account, SIGNAL(accountDestroyed()), this, SLOT(accountDestroyed()));
}

/**
 * Performs basic validity checking on a stanza
 * TODO : populate verifyStanza method and use it
 */
bool PluginManager::verifyStanza(const QString &stanza)
{
    Q_UNUSED(stanza);
    return true;
}

void PluginManager::applyOptions(const QString &plugin)
{
    auto host = hosts_.value(plugin);
    if (host) {
        host->applyOptions();
    }
}

void PluginManager::restoreOptions(const QString &plugin)
{
    auto host = hosts_.value(plugin);
    if (host) {
        host->restoreOptions();
    }
}

void PluginManager::addToolBarButton(QObject *parent, QWidget *toolbar, PsiAccount *account, const QString &contact,
                                     const QString &plugin)
{
    const int acc_id = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        if ((plugin.isEmpty() || (host->shortName() == plugin)) && host->isEnabled()) {
            host->addToolBarButton(parent, toolbar, acc_id, contact);
        }
    }
}

bool PluginManager::hasToolBarButton(const QString &plugin) const
{
    auto host = hosts_.value(plugin);
    if (host && host->isEnabled())
        return host->hasToolBarButton();
    return false;
}

void PluginManager::addGCToolBarButton(QObject *parent, QWidget *toolbar, PsiAccount *account, const QString &contact,
                                       const QString &plugin)
{
    const int acc_id = accountIds_.id(account);
    for (PluginHost *host : std::as_const(pluginsByPriority_)) {
        if ((plugin.isEmpty() || (host->shortName() == plugin)) && host->isEnabled()) {
            host->addGCToolBarButton(parent, toolbar, acc_id, contact);
        }
    }
}

bool PluginManager::hasGCToolBarButton(const QString &plugin) const
{
    auto host = hosts_.value(plugin);
    if (host && host->isEnabled())
        return host->hasGCToolBarButton();
    return false;
}

void PluginManager::setStatus(int account, const QString &status, const QString &statusMessage)
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        XMPP::Status s(status, statusMessage);
        acc->setStatus(s, false, true);
    }
}

void PluginManager::setPgpKey(int account, const QString &keyId)
{
    PsiAccount *pa = accountIds_.account(account);
    if (pa) {
        UserAccount acc  = pa->userAccount();
        acc.pgpSecretKey = keyId;
        pa->setUserAccount(acc);
    }
}

void PluginManager::removeKnownPgpKey(int account, const QString &jid)
{
    PsiAccount *pa = accountIds_.account(account);
    if (pa) {
        pa->removeKnownPgpKey(jid);
    }
}

void PluginManager::setClientVersionInfo(int account, const QVariantMap &info)
{
    PsiAccount *pa = accountIds_.account(account);
    if (pa) {
        pa->setClientVersionInfoMap(info);
    }
}

bool PluginManager::appendSysMsg(int account, const QString &jid, const QString &message)
{
    return appendMsgView(account, jid, MessageView::fromPlainText(message, MessageView::System));
}

bool PluginManager::appendSysHtmlMsg(int account, const QString &jid, const QString &message)
{
    return appendMsgView(account, jid, MessageView::fromHtml(message, MessageView::System));
}

bool PluginManager::appendMsgView(int account, const QString &jid, const MessageView &message)
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        XMPP::Jid j(jid);
        ChatDlg  *chatDlg = acc->findChatDialogEx(j);
        if (!chatDlg) {
            chatDlg = acc->findChatDialog(j, false);
        }
        if (chatDlg) {
            chatDlg->dispatchMessage(message);
            return true;
        }
        auto gcDlg = acc->findDialog<GCMainDlg *>(jid);
        if (gcDlg) {
            gcDlg->dispatchMessage(message);
            return true;
        }
    }
    return false;
}

void PluginManager::createNewEvent(int account, const QString &jid, const QString &descr, QObject *receiver,
                                   const char *slot)
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        acc->createNewPluginEvent(account, jid, descr, receiver, slot);
    }
}

void PluginManager::createNewMessageEvent(int account, QDomElement const &element)
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        acc->createNewMessageEvent(element);
    }
}

QString PluginManager::installChatLogJSDataFilter(const QString &js, PsiPlugin::Priority priority)
{
    QString uuid = QUuid::createUuid().toString();
    _messageViewJSFilters.insert(priority, { uuid, js });
    _messageViewJSFiltersTimer->start();
    return uuid;
}

void PluginManager::uninstallChatLogJSDataFilter(const QString &id)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QMutableMapIterator<PsiPlugin::Priority, std::pair<QString, QString>> it(_messageViewJSFilters);
#else
    QMutableMultiMapIterator<PsiPlugin::Priority, std::pair<QString, QString>> it(_messageViewJSFilters);
#endif

    while (it.hasNext()) {
        if (it.next().value().first == id) {
            it.remove();
            _messageViewJSFiltersTimer->start();
            return;
        }
    }
}

QStringList PluginManager::messageViewJSFilters() const
{
    if (_messageViewJSFilters.isEmpty()) {
        return QStringList();
    }
    QStringList ret;
    ret.reserve(_messageViewJSFilters.size());
    for (int i = PsiPlugin::PriorityHighest; i >= PsiPlugin::PriorityLowest; i--) {
        auto values = _messageViewJSFilters.values(PsiPlugin::Priority(i));
        for (const auto &p : values) {
            ret.append(p.second);
        }
    }
    return ret;
}

QList<QAction *> PluginManager::globalAboutMenuActions() const { return QList<QAction *>(); }

bool PluginManager::isSelf(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        PsiContact *pc = acc->findContact(XMPP::Jid(jid));
        if (pc)
            return pc->isSelf();
    }
    return false;
}

bool PluginManager::isAgent(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        PsiContact *pc = acc->findContact(XMPP::Jid(jid));
        if (pc)
            return pc->isAgent();
    }
    return false;
}

bool PluginManager::inList(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        PsiContact *pc = acc->findContact(XMPP::Jid(jid));
        if (pc)
            return pc->inList();
    }
    return false;
}

bool PluginManager::isPrivate(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        PsiContact *pc = acc->findContact(XMPP::Jid(jid));
        if (pc)
            return pc->isPrivate();
    }
    return false;
}

bool PluginManager::isConference(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        PsiContact *pc = acc->findContact(XMPP::Jid(jid));
        if (pc)
            return pc->isConference();
    }
    return false;
}

QString PluginManager::name(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        PsiContact *pc = acc->findContact(XMPP::Jid(jid));
        if (pc)
            return pc->name();
    }
    return QString();
}

QString PluginManager::status(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        PsiContact *pc = acc->findContact(XMPP::Jid(jid));
        if (pc)
            return pc->status().typeString();
    }
    return QString();
}

QString PluginManager::statusMessage(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        PsiContact *pc = acc->findContact(XMPP::Jid(jid));
        if (pc)
            return pc->status().status();
    }
    return QString();
}

QStringList PluginManager::resources(int account, const QString &jid) const
{
    QStringList l;
    PsiAccount *pa = accountIds_.account(account);
    if (pa) {
        UserListItem *u = pa->findFirstRelevant(XMPP::Jid(jid));
        if (u) {
            QMutableListIterator<UserResource> i(u->userResourceList());
            while (i.hasNext()) {
                l.push_back(i.next().name());
            }
        }
    }
    return l;
}

QString PluginManager::realJid(int account, const QString &jid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        Jid realJid = acc->realJid(XMPP::Jid(jid));
        return realJid.isNull() ? jid : realJid.full();
    }
    return jid;
}

QString PluginManager::mucNick(int account, const QString &mucJid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        auto gcDlg = acc->findDialog<GCMainDlg *>(mucJid);
        if (gcDlg) {
            return gcDlg->nick();
        }
    }
    return "";
}

QStringList PluginManager::mucNicks(int account, const QString &mucJid) const
{
    PsiAccount *acc = accountIds_.account(account);
    if (acc) {
        auto gcDlg = acc->findDialog<GCMainDlg *>(mucJid);
        if (gcDlg) {
            return gcDlg->mucRosterContent();
        }
    }
    return {};
}

bool PluginManager::hasCaps(int account, const QString &jid, const QStringList &caps)
{
    QStringList l;
    PsiAccount *pa = accountIds_.account(account);
    if (pa) {
        return pa->client()->capsManager()->features(jid).test(caps);
    }
    return false;
}

bool PluginManager::decryptMessageElement(PsiAccount *account, QDomElement &message) const
{
    for (auto const host : pluginByFile_) {
        if (host->decryptMessageElement(accountIds_.id(account), message)) {
            return true;
        }
    }
    return false;
}

bool PluginManager::encryptMessageElement(PsiAccount *account, QDomElement &message) const
{
    for (auto const host : pluginByFile_) {
        if (host->encryptMessageElement(accountIds_.id(account), message)) {
            return true;
        }
    }
    return false;
}

int AccountIds::appendAccount(PsiAccount *acc)
{
    int id = -1;
    if (acc) {
        id            = id_keys.size();
        id_keys[id]   = acc;
        acc_keys[acc] = id;
    }
    return id;
}

void AccountIds::removeAccount(PsiAccount *acc)
{
    int id = acc_keys.value(acc, -1);
    if (id != -1) {
        acc_keys.remove(acc);
        id_keys[id] = nullptr;
    }
}

void AccountIds::clear()
{
    acc_keys.clear();
    id_keys.clear();
}

PsiAccount *AccountIds::account(int id) const { return id_keys.value(id, nullptr); }

int AccountIds::id(PsiAccount *acc) const { return acc_keys.value(acc, -1); }

PluginManager *PluginManager::instance_          = nullptr;
const QString  PluginManager::loadOptionPrefix   = "plugins.auto-load";
const QString  PluginManager::pluginOptionPrefix = "plugins.options";
