/****************************************************************************
**
** Copyright (C) 2008-2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (developer.feedback@nokia.com)
**
** This file is part of the HbCore module of the UI Extensions for Mobile.
**
** GNU Lesser General Public License Usage
** 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, Nokia gives you certain additional
** rights.  These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at developer.feedback@nokia.com.
**
****************************************************************************/

#include "hbpluginnamecache_p.h"
#include <hbdevicedialogplugin.h>
#include <hbdevicedialogtrace_p.h>

#include <QDir>
#include <QMutexLocker>
#include <QThread>
#include <QStringList>

/*
    \class HbPluginNameCache
    \brief HbPluginNameCache caches plugin file names by keys

    Searching for a correct plugin can be time consuming. This class maintains a cache of plugin
    file names and corresponding keys the pluging implements and speeds up the search. Keys are
    strings the plugin returns.

    The cache watches a set of directories for changes. On directory change, cache is cleared from
    all entries with that directory and the directory is re-scanned and cache updated.
    Directory is added into cache by addWatchPath() function and removed by removeWatchPath().
    A directory that does not change and need watching, eg. a directory on a rom drive, can be
    added into the cache by calling scanDirectory().

    Directory scans are performed by a worker thread.

    HbPluginNameCache constructor takes as parameter a function pointer which it uses to read keys
    from a plugin. Class user must implement this function for plugins to be cached. Optionally
    a pointer to plugin file name filter function can be provided. If none is provided, a default
    is used.
*/

// QPluginLoader is not re-entrant if two threads are accessing the same plugin. In Windows/Linux
// where indicator/device-dialog plugin managers run in application process cache thread cannot
// be enabled as there is a chance that application is using QPluginLoader. On Symbian, device
// dialog manager runs in separate process and we can ensure that indicator/device-dialog plugins
// are not accessed outside of the manager.
#if defined(Q_OS_SYMBIAN)
#define USE_NAME_CACHE_THREAD 1
#endif

// Thread that scans directories and keeps the cache current
class HbPluginNameCacheThread : public QThread
{
public:
    HbPluginNameCacheThread(HbPluginNameCache &nameCache,
        HbPluginNameCache::GetPluginKeys getPluginKeys,
        HbPluginNameCache::PluginFileNameFilter pluginFileNameFilter);
    void scanDirectory(const QString &path);
    void cancelScan(const QString &path);
    void stop();
    QMutex &lock(){return *mMutex;}

private:
    void doDirectoryScan(const QString &path);
    void run();

private: // data
    HbPluginNameCache::GetPluginKeys mGetPluginKeys; // function to get keys from a plugin
    HbPluginNameCache::PluginFileNameFilter mPluginFileNameFilter;
    bool mExit;
    QStringList mWorkQueue; // queue for directories to be scanned
    QString mCurrentScan;  // directory currently scanned
    HbPluginNameCache &mNameCache;
    static QMutex *mMutex;
};

// Share lock between all instances of the cache.
QMutex *HbPluginNameCacheThread::mMutex = 0;

HbPluginNameCacheThread::HbPluginNameCacheThread(HbPluginNameCache &nameCache,
    HbPluginNameCache::GetPluginKeys getPluginKeys,
    HbPluginNameCache::PluginFileNameFilter pluginFileNameFilter) :
    mGetPluginKeys(getPluginKeys), mPluginFileNameFilter(pluginFileNameFilter),
    mExit(false), mNameCache(nameCache)
{
    if (!mMutex) {
        mMutex = new QMutex();
    }
}

// Add directory to be scanned
void HbPluginNameCacheThread::scanDirectory(const QString &path)
{
    QMutexLocker lock(mMutex);
    if (!mWorkQueue.contains(path)) {
        mWorkQueue.append(path);
    }
#ifdef USE_NAME_CACHE_THREAD
    if (!isRunning()) {
        mExit = false;
        start(IdlePriority);
    }
#else
    mExit = false;
    lock.unlock();
    run();
#endif
}

// Cancel a directory scan request
void HbPluginNameCacheThread::cancelScan(const QString &path)
{
    QMutexLocker lock(mMutex);
    // If scan is waiting in a queue, remove it
    int i = mWorkQueue.indexOf(path);
    if (i >= 0) {
        mWorkQueue.removeAt(i);
    }
#ifdef USE_NAME_CACHE_THREAD
    // If thread is currently scanning the path, stop and wait for it to exit
    if (isRunning() && mCurrentScan == path) {
        mExit = true;
        lock.unlock();
        stop();
        // Restart scan thread if scan queue is not empty
        if (!mWorkQueue.isEmpty()) {
            scanDirectory(mWorkQueue.first());
        }
    }
#endif
}

// Stop thread and wait for it to exit
void HbPluginNameCacheThread::stop()
{
#ifdef USE_NAME_CACHE_THREAD
    QMutexLocker lock(mMutex);
    mExit = true;
    lock.unlock();
    const unsigned long time = 5000; // 5 s
    if (!wait(time)) {
        // Thread did not exit, terminate it
        TRACE("Thread didn't exit")
        terminate();
        wait();
    }
#endif
}

// Scan directory for plugins and keys they implement.
void HbPluginNameCacheThread::doDirectoryScan(const QString &path)
{
    TRACE_ENTRY_ARGS(path)

    QString fileNameFilter = mPluginFileNameFilter();

    QDir pluginDir(path, fileNameFilter, QDir::NoSort, QDir::Files | QDir::Readable);
    foreach (const QString &fileName, pluginDir.entryList()) {
        if (mExit) {
            break;
        }

        const QString absolutePath = pluginDir.absoluteFilePath(fileName);
        HbLockedPluginLoader *loader = new HbLockedPluginLoader(*mMutex, absolutePath);
        QObject *pluginInstance = loader->instance();

        if (pluginInstance) {
            // If plugin type is correct, plugin file name and keys are saved into a cache
            mNameCache.insert(mGetPluginKeys(pluginInstance), absolutePath);

        }
        loader->unload();
        delete loader;
        loader = 0;
    }
    TRACE_EXIT
}

// Plugin scan thread function
void HbPluginNameCacheThread::run()
{
    TRACE_ENTRY

    while(true) {
        mMutex->lock();
        if (mExit || mWorkQueue.isEmpty()) {
            break;
        }
        mCurrentScan = mWorkQueue.takeFirst();
        mMutex->unlock();
        doDirectoryScan(mCurrentScan);
        mCurrentScan.clear();
    }
    mMutex->unlock();
}

// Constructor
HbPluginNameCache::HbPluginNameCache(GetPluginKeys getPluginKeys,
    PluginFileNameFilter pluginFileNameFilter, QObject *parent) :
    QObject(parent)
{
    mThread = new HbPluginNameCacheThread(*this, getPluginKeys,
        pluginFileNameFilter ? pluginFileNameFilter:&HbPluginNameCache::pluginFileNameFilter);
    connect(&mWatcher, SIGNAL(directoryChanged(const QString&)), this, SLOT(directoryChanged(const QString&)));
}

// Destructor
HbPluginNameCache::~HbPluginNameCache()
{
    mThread->stop(); // ask thread to stop and wait
    delete mThread;
    mThread = 0;
}

// Add directory watch path
void HbPluginNameCache::addWatchPath(const QString &path)
{
    TRACE_ENTRY_ARGS(path)
    QString dirPath = directoryPath(path);
    TRACE(dirPath)
    if (!dirPath.isEmpty()) {
        mWatcher.addPath(dirPath); // start watching
        directoryChanged(dirPath); // scan directory
    }
}

// Remove directory watch path
void HbPluginNameCache::removeWatchPath(const QString &path)
{
    TRACE_ENTRY_ARGS(path)
    QString dirPath = directoryPath(path);
    TRACE(dirPath)
    if (!dirPath.isEmpty()) {
#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
        const Qt::CaseSensitivity cs = Qt::CaseSensitive;
#else
        const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
#endif // Q_OS_LINUX || Q_OS_MAC
        if (mWatcher.directories().contains(dirPath, cs)) {
            mWatcher.removePath(dirPath);
            mThread->cancelScan(dirPath);
            removePath(path);
        }
    }
}

// Scan directory directory for plugins
void HbPluginNameCache::scanDirectory(const QString &path)
{
    TRACE_ENTRY_ARGS(path)
    QString dirPath = directoryPath(path);
    TRACE(dirPath)
    if (!dirPath.isEmpty()) {
        mThread->scanDirectory(dirPath);
    }
}

// Find a plugin by a key. Returns plugin file path or an empty string
QString HbPluginNameCache::find(const QString &key)
{
    TRACE_ENTRY_ARGS(key)
    QMutexLocker(&mThread->lock());
    return mCache.value(key);
}

// Get directory part of a file name
QString HbPluginNameCache::directoryPath(const QString &path)
{
    // Use QString instead of QFileInfo to handle path to ensure that file system is not
    // accessed (performance). Assume path is a directory if it ends with '/', otherwise
    // it's a file.
    QChar slash('/');
    if (path.isEmpty() || path.endsWith(slash)) {
        return path;
    } else {
        int i = path.lastIndexOf(slash);
        return i != -1 ? path.left(i + 1) : QString();
    }
}

// Print cache contents
void HbPluginNameCache::print()
{
    TRACE_ENTRY
    QMutexLocker(&mThread->lock());
    QHash<QString, QString>::iterator i = mCache.begin();
    while (i != mCache.end()) {
        TRACE(i.key() << i.value())
        ++i;
    }
}

// Update cache with \a keys and \a filePath
void HbPluginNameCache::insert(const QStringList &keys, const QString &filePath)
{
    TRACE_ENTRY_ARGS("keys" << keys << "filePath" << filePath)
    QMutexLocker(&mThread->lock());
    for (int i = 0; i < keys.size(); ++i) {
        // New entry is added into a cache or existing one is updated
        mCache.insert(keys.at(i), filePath);
    }
}

// Remove a key from from a cache
void HbPluginNameCache::remove(const QString &key)
{
    TRACE_ENTRY_ARGS(key)
    QMutexLocker(&mThread->lock());
    mCache.remove(key);
}

// Remove all keys with a \a filePath
void HbPluginNameCache::removePath(const QString &filePath)
{
    TRACE_ENTRY_ARGS(filePath)
    QMutexLocker(&mThread->lock());
    QHash<QString, QString>::iterator i = mCache.begin();
    while (i != mCache.end()) {
#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
        const Qt::CaseSensitivity cs = Qt::CaseSensitive;
#else
        const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
#endif // Q_OS_LINUX || Q_OS_MAC
        if (i.value().startsWith(filePath, cs)) {
            i = mCache.erase(i);
        } else {
            ++i;
        }
    }
}

// Generate filter for plugin file names
QString HbPluginNameCache::pluginFileNameFilter()
{
#if defined(Q_OS_LINUX)
    return QString("*.so");
#elif defined(Q_OS_MAC)
    return QString("*.dylib");
#elif defined(Q_OS_WIN32)
    return QString("*.dll");
#else
    return QString("*.qtplugin");
#endif
}

// Watched directory has changed
void HbPluginNameCache::directoryChanged(const QString &path)
{
    TRACE_ENTRY_ARGS(path)

    // Invalidate cache contents for the path
    removePath(path);
    mThread->scanDirectory(path);

    TRACE_EXIT
}

// Constructor
HbLockedPluginLoader::HbLockedPluginLoader(HbPluginNameCache &nameCache, const QString &fileName,
    QObject *parent) : mMutex(nameCache.mThread->lock())
{
    QMutexLocker locker(&mMutex);
    mLoader = new QPluginLoader(fileName, parent);
}


// Constructor
HbLockedPluginLoader::HbLockedPluginLoader(QMutex &mutex, const QString &fileName,
    QObject *parent) : mMutex(mutex)
{
    QMutexLocker locker(&mMutex);
    mLoader = new QPluginLoader(fileName, parent);
}

// Destructor
HbLockedPluginLoader::~HbLockedPluginLoader()
{
    QMutexLocker locker(&mMutex);
    delete mLoader;
}

// Locked instance
// QPluginLoader is not re-entrant if two loaders are accessing the same plugin
QObject *HbLockedPluginLoader::instance()
{
    QMutexLocker locker(&mMutex);
    return mLoader->instance();
}

// Locked load
bool HbLockedPluginLoader::load()
{
    QMutexLocker locker(&mMutex);
    return mLoader->load();
}

// Locked unload
bool HbLockedPluginLoader::unload()
{
    QMutexLocker locker(&mMutex);
    return mLoader->unload();
}
