/*
    Copyright (c) 2008 Volker Krause <vkrause@kde.org>

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

    This library is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
    License for more details.

    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to the
    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
    02110-1301, USA.
*/

#include "firstrun_p.h"
#include "dbusconnectionpool.h"
#include "servermanager.h"

#include <akonadi/agentinstance.h>
#include <akonadi/agentinstancecreatejob.h>
#include <akonadi/agentmanager.h>
#include <akonadi/agenttype.h>

#include <KConfig>
#include <KConfigGroup>
#include <KDebug>
#include <KGlobal>
#include <KProcess>
#include <KStandardDirs>

#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QDBusReply>
#include <QtCore/QDir>
#include <QtCore/QMetaMethod>
#include <QtCore/QMetaObject>

static char FIRSTRUN_DBUSLOCK[] = "org.kde.Akonadi.Firstrun.lock";

using namespace Akonadi;

Firstrun::Firstrun( QObject *parent )
  : QObject( parent ),
    mConfig( new KConfig( ServerManager::addNamespace( QLatin1String( "akonadi-firstrunrc" ) ) ) ),
    mCurrentDefault( 0 ),
    mProcess( 0 )
{
  //The code in firstrun is not safe in multi-instance mode
  Q_ASSERT( !ServerManager::hasInstanceIdentifier() );
  if ( ServerManager::hasInstanceIdentifier() ) {
    deleteLater();
    return;
  }
  kDebug();
  if ( DBusConnectionPool::threadConnection().registerService( QLatin1String( FIRSTRUN_DBUSLOCK ) ) ) {
    findPendingDefaults();
    kDebug() << mPendingDefaults;
    setupNext();
  } else {
    kDebug() << "D-Bus lock found, so someone else does the work for us already.";
    deleteLater();
  }
}

Firstrun::~Firstrun()
{
  DBusConnectionPool::threadConnection().unregisterService( QLatin1String( FIRSTRUN_DBUSLOCK ) );
  delete mConfig;
  kDebug() << "done";
}

void Firstrun::findPendingDefaults()
{
  const KConfigGroup cfg( mConfig, "ProcessedDefaults" );
  foreach ( const QString &dirName, KGlobal::dirs()->findDirs( "data", QLatin1String( "akonadi/firstrun" ) ) ) {
    const QStringList files = QDir( dirName ).entryList( QDir::Files | QDir::Readable );
    foreach ( const QString &fileName, files ) {
      const QString fullName = dirName + fileName;
      KConfig c( fullName );
      const QString id = KConfigGroup( &c, "Agent" ).readEntry( "Id", QString() );
      if ( id.isEmpty() ) {
        kWarning() << "Found invalid default configuration in " << fullName;
        continue;
      }
      if ( cfg.hasKey( id ) ) {
        continue;
      }
      mPendingDefaults << dirName + fileName;
    }
  }

#ifndef KDEPIM_NO_KRESOURCES
  // always check legacy kres for migration, their migrator might have changed again
  mPendingKres << QLatin1String( "contact" ) << QLatin1String( "calendar" );
#endif
}

#ifndef KDEPIM_NO_KRESOURCES
static QString resourceTypeForMimetype( const QStringList &mimeTypes )
{
  if ( mimeTypes.contains( QLatin1String( "text/directory" ) ) ) {
    return QString::fromLatin1( "contact" );
  }
  if ( mimeTypes.contains( QLatin1String( "text/calendar" ) ) ) {
    return QString::fromLatin1( "calendar" );
  }
  // TODO notes
  return QString();
}

void Firstrun::migrateKresType( const QString& resourceFamily )
{
  mResourceFamily = resourceFamily;
  KConfig config( QLatin1String( "kres-migratorrc" ) );
  KConfigGroup migrationCfg( &config, "Migration" );
  const bool enabled = migrationCfg.readEntry( "Enabled", false );
  const bool setupClientBridge = migrationCfg.readEntry( "SetupClientBridge", true );
  const int currentVersion = migrationCfg.readEntry( QString::fromLatin1( "Version-%1" ).arg( resourceFamily ), 0 );
  const int targetVersion = migrationCfg.readEntry( "TargetVersion", 0 );
  if ( enabled && currentVersion < targetVersion ) {
    kDebug() << "Performing migration of legacy KResource settings. Good luck!";
    mProcess = new KProcess( this );
    connect( mProcess, SIGNAL(finished(int)), SLOT(migrationFinished(int)) );
    QStringList args = QStringList() << QLatin1String( "--interactive-on-change" )
                                     << QLatin1String( "--type" ) << resourceFamily;
    if ( !setupClientBridge )
      args << QLatin1String( "--omit-client-bridge" );
    mProcess->setProgram( QLatin1String( "kres-migrator" ), args );
    mProcess->start();
    if ( !mProcess->waitForStarted() )
      migrationFinished( -1 );
  } else {
    // nothing to do
    setupNext();
  }
}

void Firstrun::migrationFinished( int exitCode )
{
  Q_ASSERT( mProcess );
  if ( exitCode == 0 ) {
    kDebug() << "KResource -> Akonadi migration has been successful";
    KConfig config( QLatin1String( "kres-migratorrc" ) );
    KConfigGroup migrationCfg( &config, "Migration" );
    const int targetVersion = migrationCfg.readEntry( "TargetVersion", 0 );
    migrationCfg.writeEntry( QString::fromLatin1( "Version-%1" ).arg( mResourceFamily ), targetVersion );
    migrationCfg.sync();
  } else if ( exitCode != 1 ) {
    // exit code 1 means it is already running, so we are probably called by a migrator instance
    kError() << "KResource -> Akonadi migration failed!";
    kError() << "command was: " << mProcess->program();
    kError() << "exit code: " << mProcess->exitCode();
    kError() << "stdout: " << mProcess->readAllStandardOutput();
    kError() << "stderr: " << mProcess->readAllStandardError();
  }

  setupNext();
}
#endif

void Firstrun::setupNext()
{
  delete mCurrentDefault;
  mCurrentDefault = 0;

  if ( mPendingDefaults.isEmpty() ) {
#ifndef KDEPIM_NO_KRESOURCES
    if ( !mPendingKres.isEmpty() ) {
      migrateKresType( mPendingKres.takeFirst() );
      return;
    }
#endif
    deleteLater();
    return;
  }

  mCurrentDefault = new KConfig( mPendingDefaults.takeFirst() );
  const KConfigGroup agentCfg = KConfigGroup( mCurrentDefault, "Agent" );

  AgentType type = AgentManager::self()->type( agentCfg.readEntry( "Type", QString() ) );
  if ( !type.isValid() ) {
    kError() << "Unable to obtain agent type for default resource agent configuration " << mCurrentDefault->name();
    setupNext();
    return;
  }

#ifndef KDEPIM_NO_KRESOURCES
  // KDE5: remove me
  // check if there is a kresource setup for this type already
  const QString kresType = resourceTypeForMimetype( type.mimeTypes() );
  if ( !kresType.isEmpty() ) {
    const QString kresCfgFile = KStandardDirs::locateLocal( "config", QString::fromLatin1( "kresources/%1/stdrc" ).arg( kresType ) );
    KConfig resCfg( kresCfgFile );
    const KConfigGroup resGroup( &resCfg, "General" );
    bool legacyResourceFound = false;
    const QStringList kresResources = resGroup.readEntry( "ResourceKeys", QStringList() )
      + resGroup.readEntry( "PassiveResourceKeys", QStringList() );
    foreach ( const QString &kresResource, kresResources ) {
      const KConfigGroup cfg( &resCfg, QString::fromLatin1( "Resource_%1" ).arg( kresResource ) );
      if ( cfg.readEntry( "ResourceType", QString() ) != QLatin1String( "akonadi" ) ) { // not a bridge
        legacyResourceFound = true;
        break;
      }
    }
    if ( legacyResourceFound ) {
      kDebug() << "ignoring " << mCurrentDefault->name() << " as there is a KResource setup for its type already.";
      KConfigGroup cfg( mConfig, "ProcessedDefaults" );
      cfg.writeEntry( agentCfg.readEntry( "Id", QString() ), QString::fromLatin1( "kres" ) );
      cfg.sync();
      setupNext();
      return;
    }
  }
#endif

  AgentInstanceCreateJob *job = new AgentInstanceCreateJob( type );
  connect( job, SIGNAL(result(KJob*)), SLOT(instanceCreated(KJob*)) );
  job->start();
}

void Firstrun::instanceCreated( KJob *job )
{
  Q_ASSERT( mCurrentDefault );

  if ( job->error() ) {
    kError() << "Creating agent instance failed for " << mCurrentDefault->name();
    setupNext();
    return;
  }

  AgentInstance instance = static_cast<AgentInstanceCreateJob*>( job )->instance();
  const KConfigGroup agentCfg = KConfigGroup( mCurrentDefault, "Agent" );
  const QString agentName = agentCfg.readEntry( "Name", QString() );
  if ( !agentName.isEmpty() )
    instance.setName( agentName );

  // agent specific settings, using the D-Bus <-> KConfigXT bridge
  const KConfigGroup settings = KConfigGroup( mCurrentDefault, "Settings" );

  QDBusInterface *iface = new QDBusInterface( QString::fromLatin1( "org.freedesktop.Akonadi.Agent.%1" ).arg( instance.identifier() ),
                                              QLatin1String( "/Settings" ), QString(),
                                              DBusConnectionPool::threadConnection(), this );
  if ( !iface->isValid() ) {
    kError() << "Unable to obtain the KConfigXT D-Bus interface of " << instance.identifier();
    setupNext();
    delete iface;
    return;
  }

  foreach ( const QString &setting, settings.keyList() ) {
    kDebug() << "Setting up " << setting << " for agent " << instance.identifier();
    const QString methodName = QString::fromLatin1( "set%1" ).arg( setting );
    const QVariant::Type argType = argumentType( iface->metaObject(), methodName );
    if ( argType == QVariant::Invalid ) {
      kError() << "Setting " << setting << " not found in agent configuration interface of " << instance.identifier();
      continue;
    }

    QVariant arg;
    if ( argType == QVariant::String ) {
      // Since a string could be a path we always use readPathEntry here,
      // that shouldn't harm any normal string settings
      arg = settings.readPathEntry( setting, QString() );
    } else
      arg = settings.readEntry( setting, QVariant( argType ) );

    const QDBusReply<void> reply = iface->call( methodName, arg );
    if ( !reply.isValid() )
      kError() << "Setting " << setting << " failed for agent " << instance.identifier();
  }

  iface->call( QLatin1String( "writeConfig" ) );

  instance.reconfigure();
  instance.synchronize();
  delete iface;

  // remember we set this one up already
  KConfigGroup cfg( mConfig, "ProcessedDefaults" );
  cfg.writeEntry( agentCfg.readEntry( "Id", QString() ), instance.identifier() );
  cfg.sync();

  setupNext();
}

QVariant::Type Firstrun::argumentType( const QMetaObject *mo, const QString &method )
{
  QMetaMethod m;
  for ( int i = 0; i < mo->methodCount(); ++i ) {
    const QString signature = QString::fromLatin1( mo->method( i ).signature() );
    if ( signature.startsWith( method ) )
      m = mo->method( i );
  }

  if ( !m.signature() )
    return QVariant::Invalid;

  const QList<QByteArray> argTypes = m.parameterTypes();
  if ( argTypes.count() != 1 )
    return QVariant::Invalid;

  return QVariant::nameToType( argTypes.first() );
}

#include "moc_firstrun_p.cpp"
