/***************************************************************************
 *   Copyright (C) 2003-2005 by liuspider                                  *
 *   liuspider@users.sourceforge.net                                       *
 *                                                                         *
 *   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.                                   *
 ***************************************************************************/
#define USE_SCIM_KDE_SETTINGS
#include "skimpluginmanager.h"

#include <qwidget.h>
#include <qtimer.h>
#include <qsignal.h> 
#include <qdir.h>

#include <kdebug.h>
#include <kparts/componentfactory.h>
#include <kplugininfo.h>
#include <kdesktopfile.h> 
#include <kapplication.h>
#include <kconfigdialog.h>
#include <kaboutdata.h>
#include <kiconloader.h>
#include <ksettings/dispatcher.h>

#include "skimglobalactions.h"
#include "skimplugin.h"
#include "utils/scimactions.h"

#if defined(Q_WS_X11)
#include <X11/Xlib.h>
#endif

using namespace scim;

#include <qvaluestack.h>
class SkimPluginManager::SkimPluginManagerPrivate
{
public:
  // All available plugins, regardless of category, and loaded or not
  QValueList<KPluginInfo *> plugins;

  // Dict of all currently loaded plugins, mapping the KPluginInfo to
  // a plugin
  QMap<KPluginInfo *, SkimPlugin *> loadedPlugins;
  struct pluginActionInfo {
    KAction * action;
    QString slot;
    KPluginInfo * pinfo;
  };
  struct extraPluginInfo {
    bool hasActions;
    bool onDemand;
    QValueList<int> actionIDs;
  };
  QMap<int, pluginActionInfo> pluginActionRepository;
  QMap<KPluginInfo *, extraPluginInfo> pluginActionInfo2ExtraInfo;
  int pluginActionRepository_id_counter;
  // The plugin manager's mode. The mode is StartingUp until loadAllPlugins()
  // has finished loading the plugins, after which it is set to Running.
  // ShuttingDown and DoneShutdown are used during Kopete shutdown by the
  // async unloading of plugins.
  enum ShutdownMode { StartingUp, Running, ShuttingDown, DoneShutdown, Reloading };
  ShutdownMode shutdownMode;

  // Plugins pending for loading
  QValueStack<QString> pluginsToLoad;
  QValueList<int> pluginActionsToActivate;
  
  // These two lists are used to overwrite enabled plugins set in the config file
  // and if one plugin keyname are included in both of them, it will be enabled
  QStringList forceEnabledPlugins;
  QStringList forceDisabledPlugins;

  bool forcedRun;
};

SkimGlobalActions* SkimPluginManager::getActionCollection()
{
  return defaultActionCollection;
}

SkimPluginManager* SkimPluginManager::s_self = 0L;
scim::ConfigModule* SkimPluginManager::cmodule = 0L;
scim::ConfigPointer SkimPluginManager::m_config;

SkimPluginManager* SkimPluginManager::self()
{
    if ( !s_self )
      new SkimPluginManager();
  
    return s_self;
}

scim::ConfigPointer SkimPluginManager::scimConfigObject()
{
    if(cmodule && !m_config.null ())
        return m_config;

    cmodule = new scim::ConfigModule ("kconfig");
    if (cmodule && cmodule->valid ()) {
        m_config = cmodule->create_config ("scim");
        if (m_config.null ()) {
            std::cerr << "  Failed to load Config Module 'kconfig'\n";
        }
    } else
        std::cerr << "  Failed to load Config Module 'kconfig'\n";
    
    if (m_config.null ()) m_config = new scim::DummyConfig ("scim");
    
    return m_config;
}

SkimPluginManager::SkimPluginManager( QStringList includePlugins, QStringList excludePlugins,
   QStringList otherArgs, QObject *, const char *name)
 : QObject(0, name), aboutData(*(KGlobal::instance()->aboutData()))
{
    if ( !s_self )
      s_self = this;
    d = new SkimPluginManagerPrivate;
    d->forceEnabledPlugins = includePlugins;
    d->forceDisabledPlugins = excludePlugins;
    d->shutdownMode = SkimPluginManagerPrivate::StartingUp;
    d->pluginActionRepository_id_counter = 0;
    d->forcedRun = false;
    
    KSettings::Dispatcher::self()->registerInstance( KGlobal::instance(), this, 
      SLOT( loadAllPlugins() ) );

    //the correct display for this program should have been set properly before this point (set by qapplication)
    QString display;
    #if defined(Q_WS_X11)
    setenv ("DISPLAY", DisplayString(QPaintDevice::x11AppDisplay()), 1);
    display = DisplayString(QPaintDevice::x11AppDisplay());
    #else
    display = getenv ("DISPLAY");
    #endif
    InputServer = new scim::SocketServerThread(this, otherArgs);

    if(!InputServer->initSocketServer(display))
    {
      d->shutdownMode = SkimPluginManagerPrivate::DoneShutdown;
      if(!otherArgs.contains("force"))
        deleteLater();
      else
        d->forcedRun = true;
    }
    else
    {
      InputServer->start();
      InputServer->start_auto_start_helpers();
    }  
    d->plugins = allAvailablePlugins(); 

    defaultActionCollection = new SkimGlobalActions(this, "Global Available Actions");

    loadAllPlugins();

    connect(kapp,SIGNAL(shutDown()),this, SLOT(shutdown()));

    connect(InputServer,SIGNAL(disconnectCompleted()),this, SLOT(shutdown()));
}

QValueList<KPluginInfo *> SkimPluginManager::allAvailablePlugins() {
    return KPluginInfo::fromServices( KTrader::self()->query( QString::fromLatin1( "Skim/Plugin" )));
}

void SkimPluginManager::pluginActionActivated(int id) {
    if(!d->loadedPlugins.contains(d->pluginActionRepository[id].pinfo)) {
      d->pluginActionsToActivate.push_back(id);
      loadPluginInternal(d->pluginActionRepository[id].pinfo->pluginName());
    } /*else {
      //if this plugin has been loaded, the action will get executed immediately because the target
      //slot has been connected to this action's activated() signal
    }*/
}

const KAboutData * SkimPluginManager::getAboutData()
{
    return &aboutData;
}

SkimPluginManager::~SkimPluginManager()
{
    if ( d->shutdownMode != SkimPluginManagerPrivate::DoneShutdown )
      kdWarning( 18018 ) << k_lineinfo << "Destructing plugin manager without going through the shutdown process!" << endl << kdBacktrace() << endl;

    // Quick cleanup of the remaining plugins, hope it helps
    QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); /* EMPTY */ )
    {
      // Remove causes the iterator to become invalid, so pre-increment first
      QMap<KPluginInfo *, SkimPlugin *>::ConstIterator nextIt( it );
      ++nextIt;
      kdWarning( 18018 ) << k_lineinfo << "Deleting stale plugin '" << it.data()->name() << "'" << endl;
      delete it.data();
      it = nextIt;
    }

    delete d;

    if(InputServer->running())
      if(!InputServer->wait(3000))
        std::cerr << "Input Socket Server exited abnormally!\n";

    ScimKdeSettings::writeConfig();

    delete ScimKdeSettings::self();
    kdDebug( 18018 ) << k_lineinfo << "SkimPluginManager exited!" << endl;
    kapp->quit();
}

QValueList<KPluginInfo *> SkimPluginManager::availablePlugins( const QString &category ) const
{
    if ( category.isEmpty() )
      return d->plugins;
  
    QValueList<KPluginInfo *> result;
    QValueList<KPluginInfo *>::ConstIterator it;
    for ( it = d->plugins.begin(); it != d->plugins.end(); ++it )
    {
      if ( ( *it )->category() == category )
        result.append( *it );
    }
  
    return result;
}

QMap<KPluginInfo *, SkimPlugin *> SkimPluginManager::loadedPlugins( const QString &category ) const
{
    QMap<KPluginInfo *, SkimPlugin *> result;
    QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it )
    {
      if ( category.isEmpty() || it.key()->category() == category )
        result.insert( it.key(), it.data() );
    }
  
    return result;
}

void SkimPluginManager::shutdown()
{
    //shutdown can be called when all connections are closed. As this signal is also needed for unload plugin
    //procedure, so we disconnect this signal from this slot
    disconnect( InputServer, SIGNAL( disconnectCompleted() ), this, SLOT(shutdown()));

    if(!d->forcedRun)
        InputServer->shutdown();

    d->shutdownMode = SkimPluginManagerPrivate::ShuttingDown;

    // Remove any pending plugins to load, we're shutting down now :)
    d->pluginsToLoad.clear();

    // Ask all plugins to unload
    QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); /* EMPTY */ )
    {
      // Remove causes the iterator to become invalid, so pre-increment first
      QMap<KPluginInfo *, SkimPlugin *>::ConstIterator nextIt( it );
      ++nextIt;
      it.data()->aboutToUnload();
      it = nextIt;
    }
  
    QTimer::singleShot( 3000, this, SLOT( slotShutdownTimeout() ) );
}

void SkimPluginManager::slotPluginReadyForUnload()
{
    // Using QObject::sender() is on purpose here, because otherwise all
    // plugins would have to pass 'this' as parameter, which makes the API
    // less clean for plugin authors
    SkimPlugin *plugin = dynamic_cast<SkimPlugin *>( const_cast<QObject *>( sender() ) );
    if ( !plugin )
    {
      kdWarning( 18018 ) << k_lineinfo << "Calling object is not a plugin!" << endl;
      return;
    }
  
    plugin->deleteLater();
}

void SkimPluginManager::slotShutdownTimeout()
{
    // When we were already done the timer might still fire.
    // Do nothing in that case.
    if ( d->shutdownMode == SkimPluginManagerPrivate::DoneShutdown )
      return;
  
    QStringList remaining;
    for ( QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it )
      remaining.append( it.data()->pluginId() );
  
    kdWarning( 18018 ) << k_lineinfo << "Some plugins didn't shutdown in time!" << endl
      << "Remaining plugins: " << remaining.join( QString::fromLatin1( ", " ) ) << endl
      << "Forcing Kopete shutdown now." << endl;
  
    slotShutdownDone();
}

void SkimPluginManager::slotShutdownDone()
{
    d->shutdownMode = SkimPluginManagerPrivate::DoneShutdown;

    if(d->forcedRun)
        deleteLater();
    else
        connect( InputServer, SIGNAL( disconnectCompleted() ), this, SLOT( deleteLater() ));
}

void SkimPluginManager::reloadAllPlugins() {

    d->shutdownMode = SkimPluginManagerPrivate::Reloading;
    
    // Ask all plugins to unload
    QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); /* EMPTY */ )
    {
      // Remove causes the iterator to become invalid, so pre-increment first
      QMap<KPluginInfo *, SkimPlugin *>::ConstIterator nextIt( it );
      ++nextIt;
      unloadPlugin(it.key());
      it = nextIt;
    }
}

void SkimPluginManager::loadAllPlugins()  // FIXME: We need session management here
{
    KIconLoader * kil = KGlobal::iconLoader();
    KConfigGroup *config = new KConfigGroup(KGlobal::config(), "Plugins");

    QStringList overloadedModules;
    for (QValueList<KPluginInfo *>::ConstIterator it = d->plugins.begin(); it != d->plugins.end(); ++it )
    {
        QString key = ( *it )->pluginName();
        ( *it )->load(config);
        QVariant v;
        if( ( *it )->isPluginEnabled() ) {
            v = ( *it )->property("X-KDE-SKIM-Overload-SCIM-Modules");
            overloadedModules += v.isValid () ? v.toStringList() : QStringList();
        }
        
        if ( (( *it )->isPluginEnabled() && !d->forceDisabledPlugins.contains(key))
          || d->forceEnabledPlugins.contains(key) )
        {
          if ( !plugin( key ) ) {
              v = ( *it )->property("X-KDE-PluginInfo-HasActions");
              bool hasActions = v.isValid () ? v.toBool() : false;
              v = ( *it )->property("X-KDE-PluginInfo-OnDemand");
              bool onDemand = v.isValid () ? v.toBool() : false;

              if(hasActions) {
                KDesktopFile kdesk(( *it )->service()->desktopEntryPath(), false, "services");
                QStringList actions = kdesk.readActions();
                SkimPluginManagerPrivate::extraPluginInfo einfo = 
                    {hasActions, onDemand, QValueList<int>()};
                for(uint ita = 0; ita < actions.size(); ita++) {
                  kdesk.setActionGroup(actions[ita]);
                  int id = d->pluginActionRepository_id_counter++;
                  SkimPluginManagerPrivate::pluginActionInfo info = {0, 
                      "1" + kdesk.readEntry("Slot") + "()", *it};
                  einfo.actionIDs.push_back(id);
                  QIconSet icon(kil->loadIcon(kdesk.readIcon(), KIcon::Toolbar));
                  if(onDemand) {
                      ScimAction * action = new ScimAction(kdesk.readName(), icon,
                      defaultActionCollection, id, kdesk.readEntry("InternalName").latin1());
                    action->setOption(scim_kde::ToolBarIconOnly);
                    action->setIcon(kdesk.readIcon());
                    info.action = action;
                    connect(action, SIGNAL(activated(int )), SLOT(pluginActionActivated(int)));
                  } else {
                    if(kdesk.readEntry("Type") == "Toggle") {
                      KToggleAction * action = new KToggleAction(kdesk.readName(), icon, "", 0, 0,
                        defaultActionCollection, kdesk.readEntry("InternalName").latin1());
                      info.action = action;
                    } else {
                      KAction * action = new KAction(kdesk.readName(), icon, "", 0, 0,
                        defaultActionCollection, kdesk.readEntry("InternalName").latin1());
                      info.action = action;
                    }
                  }
                  d->pluginActionRepository[id] = info;
                }
                d->pluginActionInfo2ExtraInfo[*it] = einfo;
              }
            if(!hasActions || !onDemand)
              d->pluginsToLoad.push( key );
          }
        } else {
          if ( plugin( key ) ) {
            unloadPlugin( *it );
          }
        }
    }

    InputServer->setOverloadedModules(overloadedModules);
    delete config;
    // Schedule the plugins to load
    QTimer::singleShot( 0, this, SLOT( slotLoadNextPlugin() ) );
}

void SkimPluginManager::slotLoadNextPlugin()
{
    if ( d->pluginsToLoad.isEmpty() )
    {
      if ( d->shutdownMode == SkimPluginManagerPrivate::StartingUp ||
          d->shutdownMode == SkimPluginManagerPrivate::Reloading )
      {
        d->shutdownMode = SkimPluginManagerPrivate::Running;
        emit allPluginsLoaded();
      }
      return;
    }
  
    QString key = d->pluginsToLoad.pop();
    loadPluginInternal( key );
  
    // Schedule the next run unconditionally to avoid code duplication on the
    // allPluginsLoaded() signal's handling. This has the added benefit that
    // the signal is delayed one event loop, so the accounts are more likely
    // to be instantiated.
    QTimer::singleShot( 0, this, SLOT( slotLoadNextPlugin() ) );
}

SkimPlugin * SkimPluginManager::loadPlugin( const QString &_pluginId, PluginLoadMode mode /* = LoadSync */ )
{
    QString pluginId = _pluginId;
  
    if ( mode == LoadSync )
    {
      return loadPluginInternal( pluginId );
    }
    else
    {
      d->pluginsToLoad.push( pluginId );
      QTimer::singleShot( 0, this, SLOT( slotLoadNextPlugin() ) );
      return 0L;
    }
}

SkimPlugin *SkimPluginManager::loadPluginInternal( const QString &pluginId )
{
    KPluginInfo *info = infoForPluginId( pluginId );
    if ( !info )
    {
      kdWarning( 18018 ) << k_funcinfo << "Unable to find a plugin named '" << pluginId << "'!" << endl;
      return 0L;
    }
  
    if ( d->loadedPlugins.contains( info ) )
      return d->loadedPlugins[ info ];
  
    int error = 0;
    SkimPlugin *plugin = KParts::ComponentFactory::createInstanceFromQuery<SkimPlugin>( 
      QString::fromLatin1( "Skim/Plugin" ),
      QString::fromLatin1( "[X-KDE-PluginInfo-Name]=='%1'" ).arg( pluginId ), 
      this, 0, QStringList(), &error );
  
    if ( plugin )
    {
      d->loadedPlugins.insert( info, plugin );
      info->setPluginEnabled( true );
  
      connect( plugin, SIGNAL( destroyed( QObject * ) ), this, SLOT( slotPluginDestroyed( QObject * ) ) );
      connect( plugin, SIGNAL( readyForUnload() ), this, SLOT( slotPluginReadyForUnload() ) );
  
      kdDebug( 18018 ) << k_lineinfo << "Successfully loaded plugin '" << pluginId << "'" << endl;
  
      if(d->pluginActionInfo2ExtraInfo.contains(info)) {
        for(uint i = 0; i < d->pluginActionInfo2ExtraInfo[info].actionIDs.size(); i++) {
          int id = d->pluginActionInfo2ExtraInfo[info].actionIDs[i];
          SkimPluginManagerPrivate::pluginActionInfo & painfo = d->pluginActionRepository[id];
          connect(painfo.action, SIGNAL(activated()), plugin, painfo.slot.latin1());
        }
      }
      for(uint i = 0; i< d->pluginActionsToActivate.size(); i++) {
        int id = d->pluginActionsToActivate[i];
        if(info == d->pluginActionRepository[id].pinfo) {
          pluginActionActivated(id);
          d->pluginActionsToActivate.remove(id);
        }
      }
      emit pluginLoaded( plugin );
    }
    else
    {
      switch( error )
      {
      case KParts::ComponentFactory::ErrNoServiceFound:
        kdDebug( 18018 ) << k_lineinfo << "No service implementing the given mimetype "
          << "and fullfilling the given constraint expression can be found." << endl;
        break;
  
      case KParts::ComponentFactory::ErrServiceProvidesNoLibrary:
        kdDebug( 18018 ) << "the specified service provides no shared library." << endl;
        break;
  
      case KParts::ComponentFactory::ErrNoLibrary:
        kdDebug( 18018 ) << "the specified library could not be loaded." << endl;
        break;
  
      case KParts::ComponentFactory::ErrNoFactory:
        kdDebug( 18018 ) << "the library does not export a factory for creating components." << endl;
        break;
  
      case KParts::ComponentFactory::ErrNoComponent:
        kdDebug( 18018 ) << "the factory does not support creating components of the specified type." << endl;
        break;
      }
  
      kdDebug( 18018 ) << k_lineinfo << "Loading plugin '" << pluginId << "' failed, KLibLoader reported error: '" << endl <<
        KLibLoader::self()->lastErrorMessage() << "'" << endl;
    }
  
    return plugin;
}

bool SkimPluginManager::unloadPlugin( const QString &spec )
{
    kdDebug() << k_lineinfo << spec << endl;
  
    QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it )
    {
      if ( it.key()->pluginName() == spec )
      {
        unloadPlugin(it.key());
        return true;
      }
    }
  
    return false;
}

bool SkimPluginManager::unloadPlugin( KPluginInfo *info )
{
    //first delete all actions registered by this plugin
    if(d->pluginActionInfo2ExtraInfo.contains(info)) {
        for(uint curaction = 0;
            curaction < d->pluginActionInfo2ExtraInfo[info].actionIDs.size();
            curaction++) {
                int id = d->pluginActionInfo2ExtraInfo[info].actionIDs[curaction];
                d->pluginActionRepository[id].action->unplugAll();
                d->pluginActionRepository[id].action->deleteLater();
                d->pluginActionRepository.remove(id);
            }
            d->pluginActionInfo2ExtraInfo.erase(info);
    }

    d->loadedPlugins[info]->aboutToUnload();
    return true;
}

void SkimPluginManager::slotPluginDestroyed( QObject *plugin )
{
    QMap<KPluginInfo *, SkimPlugin *>::Iterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it )
    {
      if ( it.data() == plugin )
      {
        kdDebug( 18018 ) << k_lineinfo << "Plugin '" << it.key()->pluginName() 
          << "' is successfully unloaded." << endl;
        d->loadedPlugins.erase( it );
        break;
      }
    }

    if (d->loadedPlugins.isEmpty())
    {
      if ( d->shutdownMode == SkimPluginManagerPrivate::ShuttingDown )
      {
        // Use a timer to make sure any pending deleteLater() calls have
        // been handled first
        QTimer::singleShot( 0, this, SLOT( slotShutdownDone() ) );
      }
      if ( d->shutdownMode == SkimPluginManagerPrivate::Reloading )
        QTimer::singleShot( 0, this, SLOT( loadAllPlugins() ) );
    }
}

SkimPlugin* SkimPluginManager::plugin( const QString &_pluginId ) const
{
    KPluginInfo *info = infoForPluginId( _pluginId );
    if ( !info )
      return 0L;

    if ( d->loadedPlugins.contains( info ) )
      return d->loadedPlugins[ info ];
    else
      return 0L;
}
QString SkimPluginManager::pluginName( const SkimPlugin *plugin ) const
{
    QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it )
    {
      if ( it.data() == plugin )
        return it.key()->name();
    }
  
    return QString::fromLatin1( "Unknown" );
}

QString SkimPluginManager::pluginId( const SkimPlugin *plugin ) const
{
    QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it )
    {
      if ( it.data() == plugin )
        return it.key()->pluginName();
    }
  
    return QString::fromLatin1( "unknown" );
}

QString SkimPluginManager::pluginIcon( const SkimPlugin *plugin ) const
{
    QMap<KPluginInfo *, SkimPlugin *>::ConstIterator it;
    for ( it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it )
    {
      if ( it.data() == plugin )
        return it.key()->icon();
    }
  
    return QString::fromLatin1( "Unknown" );
}

KPluginInfo * SkimPluginManager::infoForPluginId( const QString &pluginId ) const
{
    QValueList<KPluginInfo *>::ConstIterator it;
    for ( it = d->plugins.begin(); it != d->plugins.end(); ++it )
    {
      if ( ( *it )->pluginName() == pluginId )
        return *it;
    }
  
    return 0L;
}

bool SkimPluginManager::setPluginEnabled( const QString &_pluginId, bool enabled /* = true */ )
{
    QString pluginId = _pluginId;
  
    KConfig *config = KGlobal::config();
    config->setGroup( "Plugins" );
  
    if ( !infoForPluginId( pluginId ) )
      return false;
  
    config->writeEntry( pluginId/* + QString::fromLatin1( "Enabled" )*/, enabled );
    config->sync();
  
    return true;
}

scim::SocketServerThread* SkimPluginManager::getInputServer()
{
    return InputServer;
}
