/***************************************************************************
                          plugins.cpp  -  description
                             -------------------
    begin                : Sun Mar 17 2002
    copyright            : (C) 2002 by Vladimir Shutoff
    email                : vovan@shutoff.ru
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "simapi.h"
#include "sockfactory.h"
#include "fetch.h"
#include "exec.h"
#include "sax.h"
#include "xsl.h"
#include "builtinlogger.h"

#include <ctype.h>  // really needed?

#ifndef  LTDL_SHLIB_EXT
#if defined(QT_MACOSX_VERSION) || defined(QT_MAC) /* MacOS needs .a */
#define  LTDL_SHLIB_EXT ".a"
#else
#if defined(_WIN32) || defined(_WIN64)
#define  LTDL_SHLIB_EXT ".dll"
#else
#define  LTDL_SHLIB_EXT ".so"
#endif
#endif
#endif

#include <errno.h>
#include <qdir.h>
#include <qstringlist.h>
#include <qmessagebox.h>
#include <qapplication.h>
#include <qregexp.h>
#include <qlibrary.h>

#include <memory>
#include <algorithm>
using namespace std;

namespace SIM
{

Plugin::Plugin(unsigned base)
{
    m_base = base;
    m_current = base;
}

unsigned Plugin::registerType()
{
    return m_current++;
}

void Plugin::boundTypes()
{
    m_current = (m_current | 0x3F) + 1;
}

QWidget *Plugin::createConfigWindow(QWidget*)
{
    return NULL;
}

string Plugin::getConfig()
{
    return "";
}

typedef struct CmdDescr
{
    string arg;
    string descr;
} CmdDescr;

class PluginManagerPrivate : public EventReceiver
{
public:
    PluginManagerPrivate(int argc, char **argv);
    ~PluginManagerPrivate();
protected:
    void *processEvent(Event *e);

    bool findParam(const char *param, const char *descr, string *res);
    void usage(const char*);

    bool create(pluginInfo&);
    bool createPlugin(pluginInfo&);

    void release(pluginInfo&, bool bFree = true);
    void release(const char *name);
    void release_all(Plugin *to);

    void load(pluginInfo&);
    void load(const char *name);
    void load_all(Plugin *to);

    void saveState();
    void loadState();
    void reloadState();

    pluginInfo *getInfo(const char *name);
    pluginInfo *getInfo(unsigned n);
    bool setInfo(const char *name);

#ifndef WIN32
    unsigned long execute(const char *prg, const char *arg);
#endif

    int m_argc;
    char **m_argv;

    string app_name;
    list<string> args;
    vector<pluginInfo> plugins;
    list<string> cmds;
    list<string> descrs;

    unsigned m_base;
    bool m_bLoaded;
    bool m_bInInit;
    bool m_bAbort;

    ExecManager	 *m_exec;
    auto_ptr<BuiltinLogger> builtinLogger;

    friend class PluginManager;
};

static bool cmp_plugin(pluginInfo p1, pluginInfo p2)
{
    const char *n1 = p1.name.c_str();
    const char *n2 = p2.name.c_str();
    for (; *n1 && *n2; n1++, n2++){
        if (tolower(*n1) < tolower(*n2))
            return true;
        if (tolower(*n1) > tolower(*n2))
            return false;
    }
    return (*n1 == 0) && (*n2 != 0);
}

bool findPluginsInBuildDir(const QDir &appDir, QStringList &pluginsList)
{
    QDir pluginsDir(appDir.absFilePath("plugins"));
    log(L_DEBUG, "Searching for plugins in build directory '%s'...", static_cast<const char*>(pluginsDir.path()));
    int count = 0;
    // trunk/plugins/*
    const QStringList pluginDirs = pluginsDir.entryList("*", QDir::Dirs);
    for (QStringList::const_iterator it = pluginDirs.begin(); it != pluginDirs.end(); ++it) {
        const QDir pluginDir( *it );
        // trunk/plugins/$plugin_name/.libs/$plugin_name.so
        const QString pluginFilename = pluginsDir.filePath(QDir( pluginDir.filePath(".libs") ).
                                                           filePath(pluginDir.dirName() + LTDL_SHLIB_EXT));
        if (QFile::exists(pluginFilename)) {
            log(L_DEBUG, "Found '%s'...", static_cast<const char*>(pluginFilename));
            pluginsList.append(pluginFilename);
            count++;
            }
    }
    log(L_DEBUG, "%i plugins found.", count);
    return count > 0;
}

PluginManagerPrivate::PluginManagerPrivate(int argc, char **argv)
        : EventReceiver(LowPriority)
{
    m_bAbort = false;
    m_argc = argc;
    m_argv = argv;
    unsigned logLevel = L_ERROR | L_WARN;
#ifdef DEBUG
    logLevel |= L_DEBUG;
#endif
    builtinLogger.reset(new BuiltinLogger(logLevel));

    m_exec = new ExecManager;

    app_name = *argv;
    for (argv++, argc--; argc > 0; argv++, argc--)
        args.push_back(string(*argv));

    m_base = 0;
    m_bLoaded = false;
    m_bInInit = true;

    QStringList pluginsList;
    QDir appDir(qApp->applicationDirPath());
    if ( findPluginsInBuildDir(appDir, pluginsList)
         || findPluginsInBuildDir(appDir.path() + "/..", pluginsList)) {
        log(L_DEBUG,"Loading plugins from build directory!");
    } else {

#ifdef WIN32
        QDir pluginDir(app_file("plugins").c_str());
#else
        QDir pluginDir(PLUGIN_PATH);
#endif
        /* do some test so we can blame when sim can't access / find
           the plugins */
        pluginsList = pluginDir.entryList("*" LTDL_SHLIB_EXT);
        if (pluginsList.isEmpty()) {
            log(L_ERROR,
                "Can't access %s or directory contains no plugins!",
                pluginDir.path().ascii());
            m_bAbort = true;
            return;
        }
        m_bAbort = false;

        log(L_DEBUG,"Loading plugins from %s",pluginDir.path().ascii());
    }
    for (QStringList::iterator it = pluginsList.begin(); it != pluginsList.end(); ++it){
        QString f = QFileInfo(*it).fileName();
        int p = f.findRev('.');
        if (p > 0) f = f.left(p);
        pluginInfo info;
        info.plugin		 = NULL;
#ifdef WIN32
        info.name		 = strdup(QFile::encodeName(f.lower()));
#else
        info.name		 = strdup(QFile::encodeName(f));
#endif
        info.filePath    = QFile::encodeName(*it);
        info.cfg		 = NULL;
        info.bDisabled	 = false;
        info.bNoCreate	 = false;
        info.bFromCfg	 = false;
        info.module		 = NULL;
        info.info		 = NULL;
        info.base		 = 0;
        plugins.push_back(info);
        log(L_DEBUG,"Found plugin %s",info.name.c_str());
    }
    Event eCorePlugin(EventGetPluginInfo, (void*)"_core");
    pluginInfo *coreInfo = static_cast<pluginInfo*>(eCorePlugin.process());
    if (!coreInfo) {
        log(L_ERROR,"Fatal error: Core plugin failed to load. Aborting!");
        m_bAbort = true;
        return;
    }
    sort(plugins.begin(), plugins.end(), cmp_plugin);
    for (vector<pluginInfo>::iterator itp = plugins.begin(); itp != plugins.end(); ++itp){
        create((*itp));
        if (m_bAbort)
            return;
    }
    Event eStart(EventInit);
    if ((long)eStart.process() == -1) {
        log(L_ERROR,"EventInit failed - aborting!");
        m_bAbort = true;
        return;
    }
    for (list<string>::iterator it_args = args.begin(); it_args != args.end(); ++it_args){
        if ((*it_args).length()){
            usage((*it_args).c_str());
            break;
        }
    }
    m_bInInit = false;
}

PluginManagerPrivate::~PluginManagerPrivate()
{
    release_all(NULL);
    delete m_exec;
    setLogEnable(false);
    XSL::cleanup();
    SAXParser::cleanup();
}

void *PluginManagerPrivate::processEvent(Event *e)
{
    CmdParam *p;
#ifndef WIN32
    ExecParam *exec;
#endif
    switch (e->type()){
    case EventArg:
        p = (CmdParam*)(e->param());
        return (void*)findParam(p->arg, p->descr, p->value);
    case EventPluginGetInfo:
        return getInfo((unsigned long)(e->param()));
    case EventApplyPlugin:
        return (void*)setInfo((const char*)(e->param()));
    case EventPluginsUnload:
        release_all((Plugin*)(e->param()));
        return e->param();
    case EventPluginsLoad:
        load_all((Plugin*)(e->param()));
        return e->param();
    case EventUnloadPlugin:
        release((const char*)(e->param()));
        return e->param();
    case EventLoadPlugin:
        load((const char*)(e->param()));
        return e->param();
    case EventSaveState:
        saveState();
        break;
    case EventGetPluginInfo:
        return getInfo((const char*)(e->param()));
    case EventArgc:
        return (void*)(long)m_argc;
    case EventArgv:
        return (void*)(long)m_argv;
#ifndef WIN32
    case EventExec:
        exec = (ExecParam*)(e->param());
        return (void*)execute(exec->cmd, exec->arg);
#endif
    default:
        break;
    }
    return NULL;
}

pluginInfo *PluginManagerPrivate::getInfo(const char *name)
{
    for (unsigned n = 0; n < plugins.size(); n++){
        pluginInfo &info = plugins[n];
        if (info.name == name)
            return &info;
    }
    return NULL;
}

void PluginManagerPrivate::release_all(Plugin *to)
{
    for (int n = plugins.size() - 1; n >= 0; n--){
        pluginInfo &info = plugins[n];
        if (to && (info.plugin == to))
            break;
        if (to && info.info && (info.info->flags & (PLUGIN_PROTOCOL & ~PLUGIN_NOLOAD_DEFAULT)))
            continue;
        release(info, to != NULL);
        info.bDisabled = false;
        info.bFromCfg  = false;
        if (info.cfg){
            delete info.cfg;
            info.cfg = NULL;
        }
    }
}

void PluginManagerPrivate::load(const char *name)
{
    pluginInfo *info = getInfo(name);
    if (info)
        load(*info);
}

void PluginManagerPrivate::load(pluginInfo &info)
{
    if (info.module == NULL){
#ifdef WIN32
        string pluginName = "plugins\\";
        pluginName += info.name;
#else
        string pluginName = info.filePath;
        if( pluginName[0] != '/' ) {
            pluginName = PLUGIN_PATH;
            pluginName += "/";
            pluginName += info.name;
        }
#endif
        string fullName = app_file(pluginName.c_str());
        info.module = new QLibrary(fullName.c_str());
        if (info.module == NULL)
            fprintf(stderr, "Can't load plugin %s\n", info.name.c_str());
    }
    if (info.module == NULL)
        return;
    if (info.info == NULL){
        PluginInfo* (*getInfo)() = NULL;
        getInfo = (PluginInfo* (*)()) info.module->resolve("GetPluginInfo");
        if (getInfo == NULL){
            fprintf(stderr, "Plugin %s doesn't have the GetPluginInfo entry\n", info.name.c_str());
            release(info);
            return;
        }
        info.info = getInfo();
#ifndef WIN32
#ifdef USE_KDE
        if (!(info.info->flags & PLUGIN_KDE_COMPILE)){
            fprintf(stderr, "Plugin %s is compiled without KDE support!\n", info.name.c_str());
            release(info);
            return;
        }
#else
if (info.info->flags & PLUGIN_KDE_COMPILE){
        fprintf(stderr, "Plugin %s is compiled with KDE support!\n", info.name.c_str());
        release(info);
        return;
    }
#endif
#endif
    }
}

bool PluginManagerPrivate::create(pluginInfo &info)
{
    if (info.plugin)
        return true;
    string param;
    string descr;
    const char *short_name = info.name.c_str();
    for (; *short_name; short_name++){
        char c = *short_name;
        if ((c >= '0') && (c <= '9')) continue;
        break;
    }
    string value;
    param = "--enable-";
    param += short_name;
    if (findParam(param.c_str(), NULL, &value)){
        info.bDisabled = false;
        info.bFromCfg = true;
    }
    param = "--disable-";
    param += short_name;
    if (findParam(param.c_str(), NULL, &value)){
        info.bDisabled = true;
        info.bFromCfg = true;
    }
    if (info.bDisabled)
        return false;
    load(info);
    if (info.info == NULL)
        return false;
    if (m_bInInit && (info.info->flags & (PLUGIN_PROTOCOL & ~PLUGIN_NOLOAD_DEFAULT))){
        info.bDisabled = true;
        release(info);
        return false;
    }
    return createPlugin(info);
}

bool PluginManagerPrivate::createPlugin(pluginInfo &info)
{
    if (!info.bFromCfg && (info.info->flags & (PLUGIN_NOLOAD_DEFAULT & ~PLUGIN_DEFAULT))){
        info.bDisabled = true;
        release(info);
        return false;
    }
    log(L_DEBUG, "Load plugin %s", info.name.c_str());
    if (!m_bLoaded && !(info.info->flags & (PLUGIN_NO_CONFIG_PATH & ~PLUGIN_DEFAULT))){
        loadState();
        if (info.bDisabled || (!info.bFromCfg && (info.info->flags & (PLUGIN_NOLOAD_DEFAULT & ~PLUGIN_DEFAULT)))){
            release(info);
            return false;
        }
    }
    if (info.base == 0){
        m_base += 0x1000;
        info.base = m_base;
    }
    info.plugin = info.info->create(info.base, m_bInInit, info.cfg);
    if ((unsigned long)(info.plugin) == ABORT_LOADING){
        m_bAbort = true;
        info.plugin = NULL;
    }
    if (info.plugin == NULL){
        info.bNoCreate = true;
        info.bDisabled = true;
        return false;
    }
    if (info.cfg){
        delete info.cfg;
        info.cfg = NULL;
    }
    if (info.info->flags & PLUGIN_RELOAD){
        reloadState();
        loadState();
    }
    Event e(EventPluginChanged, &info);
    e.process();
    return true;
}

void PluginManagerPrivate::load_all(Plugin *from)
{
    if (from == (Plugin*)ABORT_LOADING){
        m_bAbort = true;
        qApp->quit();
        return;
    }
    reloadState();
    unsigned i;
    for (i = 0; i < plugins.size(); i++){
        pluginInfo &info = plugins[i];
        if (info.plugin == from)
            break;
    }
    for (; i < plugins.size(); i++)
        create(plugins[i]);
}

void PluginManagerPrivate::release(const char *name)
{
    pluginInfo *info = getInfo(name);
    if (info)
        release(*info);
}

void PluginManagerPrivate::release(pluginInfo &info, bool bFree)
{
    if (info.plugin){
        log(L_DEBUG, "Unload plugin %s", info.name.c_str());
        delete info.plugin;
        info.plugin = NULL;
        Event e(EventPluginChanged, &info);
        e.process();
    }
    if (info.module){
        if (bFree)
            delete info.module;
        info.module = NULL;
    }
    info.info = NULL;
}

pluginInfo *PluginManagerPrivate::getInfo(unsigned n)
{
    if (n >= plugins.size())
        return NULL;
    pluginInfo &info = plugins[n];
    return &info;
}

bool PluginManagerPrivate::setInfo(const char *name)
{
    pluginInfo *info = getInfo(name);
    if (info == NULL)
        return false;
    if (info->bDisabled){
        if (info->plugin == NULL)
            return false;
        release(*info);
        load(*info);
        return true;
    }
    if (info->plugin)
        return false;
    info->bFromCfg = true;
    load(*info);
    createPlugin(*info);
    return true;
}

static char PLUGINS_CONF[] = "plugins.conf";
static char ENABLE[] = "enable";
static char DISABLE[] = "disable";
static char BACKUP_SUFFIX[] = "~";

void PluginManagerPrivate::saveState()
{
    if (m_bAbort)
        return;
    getContacts()->save();
    string cfgName = user_file(PLUGINS_CONF);
    QFile f(QFile::decodeName((cfgName + BACKUP_SUFFIX).c_str())); // use backup file for this ...
    if (!f.open(IO_WriteOnly | IO_Truncate)){
        log(L_ERROR, "Can't create %s", (const char*)f.name().local8Bit());
        return;
    }
    for (unsigned i = 0; i < plugins.size(); i++){
        pluginInfo &info = plugins[i];
        string line = "[";
        line += info.name;
        line += "]\n";
        line += info.bDisabled ? DISABLE : ENABLE;
        line += ",";
        line += number(info.base);
        line += "\n";
        f.writeBlock(line.c_str(), line.length());
        if (info.plugin){
            string cfg = info.plugin->getConfig();
            if (cfg.length()){
                f.writeBlock(cfg.c_str(), cfg.length());
                f.writeBlock("\n", 1);
            }
        }
    }

    const int status = f.status();
    const QString errorMessage = f.errorString();
    f.close();
    if (status != IO_Ok) {
        log(L_ERROR, "I/O error during writing to file %s : %s", (const char*)f.name().local8Bit(), (const char*)errorMessage.local8Bit());
        return;
    }

    // rename to normal file
    QFileInfo fileInfo(f.name());
    QString desiredFileName = fileInfo.fileName();
    desiredFileName = desiredFileName.left(desiredFileName.length() - strlen(BACKUP_SUFFIX));
#ifdef WIN32
    fileInfo.dir().remove(desiredFileName);
#endif
    if (!fileInfo.dir().rename(fileInfo.fileName(), desiredFileName)) {
        log(L_ERROR, "Can't rename file %s to %s", (const char*)fileInfo.fileName().local8Bit(), (const char*)desiredFileName.local8Bit());
        return;
    }
}

const unsigned long NO_PLUGIN = (unsigned long)(-1);

void PluginManagerPrivate::reloadState()
{
    m_bLoaded = false;
    for (unsigned i = 0; i < plugins.size(); i++){
        pluginInfo &info = plugins[i];
        if (info.cfg){
            delete info.cfg;
            info.cfg = NULL;
        }
    }
}

void PluginManagerPrivate::loadState()
{
    if (m_bLoaded) return;

    m_bLoaded = true;
    string cfgName = user_file(PLUGINS_CONF);
    QFile f(QFile::decodeName(cfgName.c_str()));

    if (!f.exists()) {
        /* Maybe first start ? */
        QDir dir(user_file(NULL).c_str());
        if (!dir.exists()) {
            log(L_WARN, "Creating directory %s",dir.absPath().ascii());
            if (!dir.mkdir(dir.absPath())) {
                log(L_ERROR, "Can't create directory %s",dir.absPath().ascii());
                return;
            }
        }
        if (f.open(IO_WriteOnly))
            f.close();
        else {
            log(L_ERROR, "Can't create %s",f.name().ascii());
            return;
        }
    }

    if (!f.open(IO_ReadOnly)){
        log(L_ERROR, "Can't open %s", f.name().ascii());
        return;
    }

    Buffer cfg;
    cfg.init(f.size());
    int n = f.readBlock(cfg.data(), f.size());

    if (n < 0){
        log(L_ERROR, "Can't read %s", f.name().ascii());
        return;
    }

    bool continous=TRUE;
    while(continous) {

        string section = cfg.getSection();

        if (section.empty())
            return;
        unsigned long i = NO_PLUGIN;
        for (unsigned n = 0; n < plugins.size(); n++)
            if (section == plugins[n].name){
                i = n;
                break;
            }

        if (i == NO_PLUGIN)
            continue;

        pluginInfo &info = plugins[i];
        const char *line = cfg.getLine();

        if (line == NULL)
            continue;
        string token = getToken(line, ',');
        if (token == ENABLE){
            info.bDisabled = false;
            info.bFromCfg  = true;
        }
        else if (token == DISABLE){
            info.bDisabled = true;
            info.bFromCfg  = true;
        }
        else {continue;}

        info.base = atol(line);

        if (info.base > m_base)
            m_base = info.base;

        if (cfg.readPos() < cfg.writePos()){
            plugins[i].cfg = new Buffer;
            plugins[i].cfg->pack(cfg.data(cfg.readPos()), cfg.writePos() - cfg.readPos());
        }
    }
}

bool PluginManagerPrivate::findParam(const char *p, const char *descriptor, string *value)
{
    if (descriptor){
        cmds.push_back(string(p));
        descrs.push_back(string(descriptor));
    }
    (*value) = "";
    if (*p && p[strlen(p) - 1] == ':'){
        unsigned size = strlen(p) - 1;
        for (list<string>::iterator it = args.begin(); it != args.end(); ++it){
            if ((*it).length() < size) continue;
            if (memcmp((*it).c_str(), p, size)) continue;
            *value = (*it).c_str() + size;
            if (value->length()){
                *it = "";
                return true;
            }
            ++it;
            if (it != args.end()){
                *value = (*it);
                *it = "";
                --it;
                *it = "";
                return true;
            } else {
                return true;
            }
        }
    }else{
        for (list<string>::iterator it = args.begin(); it != args.end(); ++it){
            if (strcmp((*it).c_str(), p)) continue;
            *value = (*it);
            *it = "";
            return true;
        }
    }
    return false;
}

void PluginManagerPrivate::usage(const char *err)
{
    QString title = i18n("Bad option %1") .arg(err);
    QString text = i18n("Usage: %1 ") .arg(app_name.c_str());
    QString comment;
    list<string>::iterator itc = cmds.begin();
    list<string>::iterator itd = descrs.begin();
    for (; itc != cmds.end(); ++itc, ++itd){
        string p = *itc;
        bool bParam = false;
        if (p[p.length() - 1] == ':'){
            bParam = true;
            p = p.substr(0, p.length() - 1);
        }
        text += "[";
        text += p.c_str();
        if (bParam) text += "<arg>";
        text += "] ";
        comment += "\t";
        comment += p.c_str();
        if (bParam) comment += "<arg>";
        comment += "\t - ";
        comment += i18n((*itd).c_str());
        comment += "\n";
    }
    text += "\n";
    text += comment;
    QMessageBox::critical(NULL, title, text, "Quit", 0);
}

#ifndef WIN32
unsigned long PluginManagerPrivate::execute(const char *prg, const char *arg)
{
    if (*prg == 0)
        return 0;
    QString p = QString::fromLocal8Bit(prg);
    if (p.find("%s") >= 0){
        p.replace(QRegExp("%s"), arg);
    }else{
        p += " ";
        p += QString::fromLocal8Bit(arg);
    }
    log(L_DEBUG, "Exec: %s", (const char*)p.local8Bit());
    QStringList s = QStringList::split(" ", p);
    char **arglist = new char*[s.count()+1];
    unsigned i = 0;
    for ( QStringList::Iterator it = s.begin(); it != s.end(); ++it, i++ ) {
        string arg;
        arg = (*it).local8Bit();
        arglist[i] = strdup(arg.c_str());
    }
    arglist[i] = NULL;
    pid_t child = fork();
    if (child == -1){
        log(L_WARN, "Can't fork: %s", strerror(errno));
        for (char **p = arglist; *p != NULL; p++)
            free(*p);
        delete[] arglist;
        return 0;
    }
    if(!child) {
        execvp(arglist[0], arglist);
        // when we're here an error occured ...
        // a write to the logoutput isn't possible because we haven't
        // these descriptors anymore - so we need printf
        printf("can't execute %s: %s", arglist[0], strerror(errno));
        _exit(-1);
    }
    for (char **p = arglist; *p != NULL; p++)
        free(*p);
    delete[] arglist;
    return child;
}
#endif

void createIcons();
void deleteIcons();

PluginManager::PluginManager(int argc, char **argv)
{
    EventReceiver::initList();
    factory = new SIMSockets;
    contacts = new ContactList;
    FetchManager::manager = new FetchManager;
    createIcons();
    p = new PluginManagerPrivate(argc, argv);
}

void deleteResolver();

PluginManager::~PluginManager()
{
    Event e(EventQuit);
    e.process();
    contacts->clearClients();
    delete p;
    delete FetchManager::manager;
    delete contacts;
    delete factory;
    deleteIcons();
    EventReceiver::destroyList();
    deleteResolver();
}

bool PluginManager::isLoaded()
{
    return !p->m_bAbort;
}

ContactList *PluginManager::contacts = NULL;
SocketFactory *PluginManager::factory = NULL;

}
