/*
    Copyright (c) 2007 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 "changerecorder_p.h"

#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtCore/QSettings>
#include <QtCore/QFileInfo>

using namespace Akonadi;

ChangeRecorderPrivate::ChangeRecorderPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_,
                                             ChangeRecorder *parent)
  : MonitorPrivate( dependenciesFactory_, parent ),
     settings( 0 ),
     enableChangeRecording( true ),
    m_lastKnownNotificationsCount( 0 ),
    m_startOffset( 0 ),
    m_needFullSave( true )
{
}

int ChangeRecorderPrivate::pipelineSize() const
{
  if ( enableChangeRecording )
    return 0; // we fill the pipeline ourselves when using change recording
  return MonitorPrivate::pipelineSize();
}

void ChangeRecorderPrivate::slotNotify(const Akonadi::NotificationMessageV3::List &msgs)
{
  Q_Q( ChangeRecorder );
  const int oldChanges = pendingNotifications.size();
  // with change recording disabled this will automatically take care of dispatching notification messages and saving
  MonitorPrivate::slotNotify( msgs );
  if ( enableChangeRecording && pendingNotifications.size() != oldChanges ) {
    emit q->changesAdded();
  }
}

// The QSettings object isn't actually used anymore, except for migrating old data
// and it gives us the base of the filename to use. This is all historical.
QString ChangeRecorderPrivate::notificationsFileName() const
{
  return settings->fileName() + QLatin1String( "_changes.dat" );
}

void ChangeRecorderPrivate::loadNotifications()
{
    pendingNotifications.clear();
    Q_ASSERT(pipeline.isEmpty());
    pipeline.clear();

    const QString changesFileName = notificationsFileName();

    /**
     * In an older version we recorded changes inside the settings object, however
     * for performance reasons we changed that to store them in a separated file.
     * If this file doesn't exists, it means we run the new version the first time,
     * so we have to read in the legacy list of changes first.
     */
    if ( !QFile::exists( changesFileName ) ) {
      QStringList list;
      settings->beginGroup( QLatin1String( "ChangeRecorder" ) );
      const int size = settings->beginReadArray( QLatin1String( "change" ) );

      for ( int i = 0; i < size; ++i ) {
        settings->setArrayIndex( i );
        NotificationMessageV3 msg;
        msg.setSessionId( settings->value( QLatin1String( "sessionId" ) ).toByteArray() );
        msg.setType( (NotificationMessageV2::Type)settings->value( QLatin1String( "type" ) ).toInt() );
        msg.setOperation( (NotificationMessageV2::Operation)settings->value( QLatin1String( "op" ) ).toInt() );
        msg.addEntity( settings->value( QLatin1String( "uid" ) ).toLongLong(),
                       settings->value( QLatin1String( "rid" ) ).toString(),
                       QString(),
                       settings->value( QLatin1String( "mimeType" ) ).toString() );
        msg.setResource( settings->value( QLatin1String( "resource" ) ).toByteArray() );
        msg.setParentCollection( settings->value( QLatin1String( "parentCol" ) ).toLongLong() );
        msg.setParentDestCollection( settings->value( QLatin1String( "parentDestCol" ) ).toLongLong() );
        list = settings->value( QLatin1String( "itemParts" ) ).toStringList();
        QSet<QByteArray> itemParts;
        Q_FOREACH( const QString &entry, list )
          itemParts.insert( entry.toLatin1() );
        msg.setItemParts( itemParts );
        pendingNotifications << msg;
      }

      settings->endArray();

      // save notifications to the new file...
      saveNotifications();

      // ...delete the legacy list...
      settings->remove( QString() );
      settings->endGroup();

      // ...and continue as usually
    }

    QFile file( changesFileName );
    if ( file.open( QIODevice::ReadOnly ) ) {
      m_needFullSave = false;
      pendingNotifications = loadFrom( &file );
    } else {
      m_needFullSave = true;
    }
    notificationsLoaded();
}

static const quint64 s_currentVersion = Q_UINT64_C(0x000300000000);
static const quint64 s_versionMask    = Q_UINT64_C(0xFFFF00000000);
static const quint64 s_sizeMask       = Q_UINT64_C(0x0000FFFFFFFF);

QQueue<NotificationMessageV3> ChangeRecorderPrivate::loadFrom(QIODevice *device)
{
  QDataStream stream( device );
  stream.setVersion( QDataStream::Qt_4_6 );

  QByteArray sessionId, resource, destinationResource;
  int type, operation, entityCnt;
  quint64 uid, parentCollection, parentDestCollection;
  QString remoteId, mimeType, remoteRevision;
  QSet<QByteArray> itemParts, addedFlags, removedFlags;
  QSet<qint64> addedTags, removedTags;

  QQueue<NotificationMessageV3> list;

  quint64 sizeAndVersion;
  stream >> sizeAndVersion;

  const quint64 size = sizeAndVersion & s_sizeMask;
  const quint64 version = (sizeAndVersion & s_versionMask) >> 32;

  quint64 startOffset = 0;
  if ( version >= 1 ) {
    stream >> startOffset;
  }

  // If we skip the first N items, then we'll need to rewrite the file on saving.
  // Also, if the file is old, it needs to be rewritten.
  m_needFullSave = startOffset > 0 || version == 0;

  for ( quint64 i = 0; i < size && !stream.atEnd(); ++i ) {
    NotificationMessageV2 msg;

    if ( version == 1 ) {
      stream >> sessionId;
      stream >> type;
      stream >> operation;
      stream >> uid;
      stream >> remoteId;
      stream >> resource;
      stream >> parentCollection;
      stream >> parentDestCollection;
      stream >> mimeType;
      stream >> itemParts;

      if ( i < startOffset )
        continue;

      msg.setSessionId( sessionId );
      msg.setType( static_cast<NotificationMessageV2::Type>( type ) );
      msg.setOperation( static_cast<NotificationMessageV2::Operation>( operation ) );
      msg.addEntity( uid, remoteId, QString(), mimeType );
      msg.setResource( resource );
      msg.setParentCollection( parentCollection );
      msg.setParentDestCollection( parentDestCollection );
      msg.setItemParts( itemParts );

    } else if ( version == 2 || version == 3 ) {

      NotificationMessageV3 msg;

      stream >> sessionId;
      stream >> type;
      stream >> operation;
      stream >> entityCnt;
      for ( int j = 0; j < entityCnt; ++j ) {
        stream >> uid;
        stream >> remoteId;
        stream >> remoteRevision;
        stream >> mimeType;
        msg.addEntity( uid, remoteId, remoteRevision, mimeType );
      }
      stream >> resource;
      stream >> destinationResource;
      stream >> parentCollection;
      stream >> parentDestCollection;
      stream >> itemParts;
      stream >> addedFlags;
      stream >> removedFlags;
      if ( version == 3 ) {
        stream >> addedTags;
        stream >> removedTags;
      }

      if ( i < startOffset )
        continue;

      msg.setSessionId( sessionId );
      msg.setType( static_cast<NotificationMessageV2::Type>( type ) );
      msg.setOperation( static_cast<NotificationMessageV2::Operation>( operation ) );
      msg.setResource( resource );
      msg.setDestinationResource( destinationResource );
      msg.setParentCollection( parentCollection );
      msg.setParentDestCollection( parentDestCollection );
      msg.setItemParts( itemParts );
      msg.setAddedFlags( addedFlags );
      msg.setRemovedFlags( removedFlags );
      msg.setAddedTags( addedTags );
      msg.setRemovedTags( removedTags );

      list << msg;
    }
  }

  return list;
}

QString ChangeRecorderPrivate::dumpNotificationListToString() const
{
  if ( !settings )
    return QString::fromLatin1( "No settings set in ChangeRecorder yet." );
  QString result;
  const QString changesFileName = notificationsFileName();
  QFile file( changesFileName );
  if ( !file.open( QIODevice::ReadOnly ) )
    return QString::fromLatin1( "Error reading " ) + changesFileName;

  QDataStream stream( &file );
  stream.setVersion( QDataStream::Qt_4_6 );

  QByteArray sessionId, resource, destResource;
  int type, operation, entityCnt;
  quint64 parentCollection, parentDestCollection;
  QString remoteId, remoteRevision, mimeType;
  QSet<QByteArray> itemParts, addedFlags, removedFlags;
  QSet<qint64> addedTags, removedTags;
  QVariantList items;

  QStringList list;

  quint64 sizeAndVersion;
  stream >> sizeAndVersion;

  const quint64 size = sizeAndVersion & s_sizeMask;
  const quint64 version = (sizeAndVersion & s_versionMask) >> 32;

  quint64 startOffset = 0;
  if ( version >= 1 ) {
    stream >> startOffset;
  }

  for ( quint64 i = 0; i < size && !stream.atEnd(); ++i ) {
    stream >> sessionId;
    stream >> type;
    stream >> operation;
    stream >> entityCnt;
    for ( int j = 0; j < entityCnt; ++j ) {
      QVariantMap map;
      stream >> map[ QLatin1String( "uid" ) ];
      stream >> map[ QLatin1String( "remoteId" ) ];
      stream >> map[ QLatin1String( "remoteRevision" ) ];
      stream >> map[ QLatin1String( "mimeType" ) ];
      items << map;
    }
    stream >> resource;
    stream >> destResource;
    stream >> parentCollection;
    stream >> parentDestCollection;
    stream >> itemParts;
    stream >> addedFlags;
    stream >> removedFlags;
    if ( version == 3 ) {
      stream >> addedTags;
      stream >> removedTags;
    }

    if ( i < startOffset )
        continue;

    QString typeString;
    switch ( type ) {
    case NotificationMessageV2::Collections:
      typeString = QLatin1String( "Collections" );
      break;
    case NotificationMessageV2::Items:
      typeString = QLatin1String( "Items" );
      break;
    case NotificationMessageV2::Tags:
      typeString = QLatin1String( "Tags" );
      break;
    default:
      typeString = QLatin1String( "InvalidType" );
      break;
    };

    QString operationString;
    switch ( operation ) {
    case NotificationMessageV2::Add:
      operationString = QLatin1String( "Add" );
      break;
    case NotificationMessageV2::Modify:
      operationString = QLatin1String( "Modify" );
      break;
    case NotificationMessageV2::ModifyFlags:
      operationString = QLatin1String( "ModifyFlags" );
      break;
    case NotificationMessageV2::ModifyTags:
      operationString = QLatin1String( "ModifyTags" );
      break;
    case NotificationMessageV2::Move:
      operationString = QLatin1String( "Move" );
      break;
    case NotificationMessageV2::Remove:
      operationString = QLatin1String( "Remove" );
      break;
    case NotificationMessageV2::Link:
      operationString = QLatin1String( "Link" );
      break;
    case NotificationMessageV2::Unlink:
      operationString = QLatin1String( "Unlink" );
      break;
    case NotificationMessageV2::Subscribe:
      operationString = QLatin1String( "Subscribe" );
      break;
    case NotificationMessageV2::Unsubscribe:
      operationString = QLatin1String( "Unsubscribe" );
      break;
    default:
      operationString = QLatin1String( "InvalidOp" );
      break;
    };

    QStringList itemPartsList, addedFlagsList, removedFlagsList, addedTagsList, removedTagsList;
    foreach( const QByteArray &b, itemParts )
      itemPartsList.push_back( QString::fromLatin1(b) );
    foreach( const QByteArray &b, addedFlags )
      addedFlagsList.push_back( QString::fromLatin1(b) );
    foreach( const QByteArray &b, removedFlags )
      removedFlagsList.push_back( QString::fromLatin1(b) );
    foreach( qint64 id, addedTags )
      addedTagsList.push_back( QString::number( id ) );
    foreach( qint64 id, removedTags )
      removedTagsList.push_back( QString::number( id ) ) ;

    const QString entry = QString::fromLatin1("session=%1 type=%2 operation=%3 items=%4 resource=%5 destResource=%6 parentCollection=%7 parentDestCollection=%8 itemParts=%9 addedFlags=%10 removedFlags=%11 addedTags=%12 removedTags=%13")
                          .arg( QString::fromLatin1( sessionId ) )
                          .arg( typeString )
                          .arg( operationString )
                          .arg( QVariant(items).toString() )
                          .arg( QString::fromLatin1( resource ) )
                          .arg( QString::fromLatin1( destResource ) )
                          .arg( parentCollection )
                          .arg( parentDestCollection )
                          .arg( itemPartsList.join(QLatin1String(", " )) )
                          .arg( addedFlagsList.join(QLatin1String( ", " )) )
                          .arg( removedFlagsList.join(QLatin1String( ", " )) )
                          .arg( addedTagsList.join(QLatin1String(", ")) )
                          .arg( removedTagsList.join(QLatin1String(", ")) );

    result += entry + QLatin1Char('\n');
  }

  return result;
}

void ChangeRecorderPrivate::addToStream(QDataStream &stream, const NotificationMessageV3 &msg)
{
  stream << msg.sessionId();
  stream << int(msg.type());
  stream << int(msg.operation());
  stream << msg.entities().count();
  Q_FOREACH( const NotificationMessageV2::Entity &entity, msg.entities() ) {
    stream << quint64(entity.id);
    stream << entity.remoteId;
    stream << entity.remoteRevision;
    stream << entity.mimeType;
  }
  stream << msg.resource();
  stream << msg.destinationResource();
  stream << quint64(msg.parentCollection());
  stream << quint64(msg.parentDestCollection());
  stream << msg.itemParts();
  stream << msg.addedFlags();
  stream << msg.removedFlags();
  stream << msg.addedTags();
  stream << msg.removedTags();
}

void ChangeRecorderPrivate::writeStartOffset()
{
  if ( !settings )
    return;

  QFile file( notificationsFileName() );
  if ( !file.open( QIODevice::ReadWrite ) ) {
    qWarning() << "Could not update notifications in file" << file.fileName();
    return;
  }

  // Skip "countAndVersion"
  file.seek( 8 );

  //kDebug() << "Writing start offset=" << m_startOffset;

  QDataStream stream( &file );
  stream.setVersion( QDataStream::Qt_4_6 );
  stream << static_cast<quint64>(m_startOffset);

  // Everything else stays unchanged
}

void ChangeRecorderPrivate::saveNotifications()
{
  if ( !settings )
    return;

  QFile file( notificationsFileName() );
  QFileInfo info( file );
  if ( !QFile::exists( info.absolutePath() ) ) {
    QDir dir;
    dir.mkpath( info.absolutePath() );
  }
  if ( !file.open( QIODevice::WriteOnly ) ) {
    qWarning() << "Could not save notifications to file" << file.fileName();
    return;
  }
  saveTo(&file);
  m_needFullSave = false;
  m_startOffset = 0;
}

void ChangeRecorderPrivate::saveTo(QIODevice *device)
{
  // Version 0 of this file format was writing a quint64 count, followed by the notifications.
  // Version 1 bundles a version number into that quint64, to be able to detect a version number at load time.

  const quint64 countAndVersion = static_cast<quint64>(pendingNotifications.count()) | s_currentVersion;

  QDataStream stream( device );
  stream.setVersion( QDataStream::Qt_4_6 );

  stream << countAndVersion;
  stream << quint64(0); // no start offset

  //kDebug() << "Saving" << pendingNotifications.count() << "notifications (full save)";

  for ( int i = 0; i < pendingNotifications.count(); ++i ) {
    const NotificationMessageV3 msg = pendingNotifications.at( i );
    addToStream( stream, msg );
  }
}

void ChangeRecorderPrivate::notificationsEnqueued( int count )
{
  // Just to ensure the contract is kept, and these two methods are always properly called.
  if (enableChangeRecording) {
    m_lastKnownNotificationsCount += count;
    if ( m_lastKnownNotificationsCount != pendingNotifications.count() ) {
      kWarning() << this << "The number of pending notifications changed without telling us! Expected"
                 << m_lastKnownNotificationsCount << "but got" << pendingNotifications.count()
                 << "Caller just added" << count;
      Q_ASSERT( pendingNotifications.count() == m_lastKnownNotificationsCount );
    }

    saveNotifications();
  }
}

void ChangeRecorderPrivate::dequeueNotification()
{
  pendingNotifications.dequeue();

  if (enableChangeRecording) {

    Q_ASSERT( pendingNotifications.count() == m_lastKnownNotificationsCount - 1 );
    --m_lastKnownNotificationsCount;

    if ( m_needFullSave || pendingNotifications.isEmpty() ) {
      saveNotifications();
    } else {
      ++m_startOffset;
      writeStartOffset();
    }
  }
}

void ChangeRecorderPrivate::notificationsErased()
{
  if (enableChangeRecording) {
    m_lastKnownNotificationsCount = pendingNotifications.count();
    m_needFullSave = true;
    saveNotifications();
  }
}

void ChangeRecorderPrivate::notificationsLoaded()
{
  m_lastKnownNotificationsCount = pendingNotifications.count();
  m_startOffset = 0;
}
