/*
    Copyright (c) 2007 Volker Krause <vkrause@kde.org>
    Copyright (c) 2013 Daniel Vrátil <dvratil@redhat.com>

    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 "notificationmessagev2_p.h"
#include "notificationmessagev2_p_p.h"
#include "notificationmessage_p.h"
#include "imapparser_p.h"

#include <QtCore/QDebug>
#include <QtCore/QHash>
#include <QtDBus/QDBusMetaType>
#include <qdbusconnection.h>

using namespace Akonadi;

class NotificationMessageV2::Private : public QSharedData
{
  public:
    Private()
      : QSharedData()
      , type( InvalidType )
      , operation( InvalidOp )
      , parentCollection( -1 )
      , parentDestCollection( -1 )
    {
    }

    Private( const Private &other )
      : QSharedData( other )
    {
      sessionId = other.sessionId;
      type = other.type;
      operation = other.operation;
      items = other.items;
      resource = other.resource;
      destResource = other.destResource;
      parentCollection = other.parentCollection;
      parentDestCollection = other.parentDestCollection;
      parts = other.parts;
      addedFlags = other.addedFlags;
      removedFlags = other.removedFlags;
      addedTags = other.addedTags;
      removedTags = other.removedTags;
    }

    QByteArray sessionId;
    NotificationMessageV2::Type type;
    NotificationMessageV2::Operation operation;
    QMap<Id, NotificationMessageV2::Entity> items;
    QByteArray resource;
    QByteArray destResource;
    Id parentCollection;
    Id parentDestCollection;
    QSet<QByteArray> parts;
    QSet<QByteArray> addedFlags;
    QSet<QByteArray> removedFlags;
    QSet<qint64> addedTags;
    QSet<qint64> removedTags;
};

NotificationMessageV2::NotificationMessageV2():
  d( new Private )
{
}

NotificationMessageV2::NotificationMessageV2( const NotificationMessageV2 &other ):
  d( other.d )
{
}

NotificationMessageV2::~NotificationMessageV2()
{
}

NotificationMessageV2 &NotificationMessageV2::operator=( const NotificationMessageV2 &other )
{
  if ( this != &other ) {
    d = other.d;
  }

  return *this;
}

bool NotificationMessageV2::operator==( const NotificationMessageV2 &other ) const
{
  return d->operation == other.d->operation
          && d->parts == other.d->parts
          && d->addedFlags == other.d->addedFlags
          && d->removedFlags == other.d->removedFlags
          && d->addedTags == other.d->addedTags
          && d->removedTags == other.d->removedTags
          && NotificationMessageHelpers::compareWithoutOpAndParts( *this, other );
}

void NotificationMessageV2::registerDBusTypes()
{
  qDBusRegisterMetaType<Akonadi::NotificationMessageV2>();
  qDBusRegisterMetaType<Akonadi::NotificationMessageV2::Entity>();
  qDBusRegisterMetaType<Akonadi::NotificationMessageV2::List>();
  qDBusRegisterMetaType<Akonadi::NotificationMessageV2::Type>();
  qDBusRegisterMetaType<QVector<QByteArray> >();
  qDBusRegisterMetaType<QVector<qint64> >();
}

bool NotificationMessageV2::isValid() const
{
  return d->operation != Akonadi::NotificationMessageV2::InvalidOp
         && d->type != Akonadi::NotificationMessageV2::InvalidType
         && !d->items.isEmpty();
}

void NotificationMessageV2::addEntity( Id id, const QString &remoteId, const QString &remoteRevision, const QString &mimeType )
{
  NotificationMessageV2::Entity item;
  item.id = id;
  item.remoteId = remoteId;
  item.remoteRevision = remoteRevision;
  item.mimeType = mimeType;

  d->items.insert( id, item );
}

void NotificationMessageV2::setEntities( const QList<NotificationMessageV2::Entity> &items )
{
  clearEntities();
  Q_FOREACH ( const NotificationMessageV2::Entity &item, items ) {
    d->items.insert( item.id, item );
  }
}

void NotificationMessageV2::clearEntities()
{
  d->items.clear();
}

QMap<NotificationMessageV2::Id, NotificationMessageV2::Entity> NotificationMessageV2::entities() const
{
  return d->items;
}

NotificationMessageV2::Entity NotificationMessageV2::entity( NotificationMessageV2::Id id ) const
{
  return d->items.value( id );
}

QList<NotificationMessageV2::Id> NotificationMessageV2::uids() const
{
  return d->items.keys();
}

QByteArray NotificationMessageV2::sessionId() const
{
  return d->sessionId;
}

void NotificationMessageV2::setSessionId( const QByteArray &sessionId )
{
  d->sessionId = sessionId;
}

NotificationMessageV2::Type NotificationMessageV2::type() const
{
  return d->type;
}

void NotificationMessageV2::setType( Type type )
{
  d->type = type;
}

NotificationMessageV2::Operation NotificationMessageV2::operation() const
{
  return d->operation;
}

void NotificationMessageV2::setOperation( Operation operation )
{
  d->operation = operation;
}

QByteArray NotificationMessageV2::resource() const
{
  return d->resource;
}

void NotificationMessageV2::setResource( const QByteArray &resource )
{
  d->resource = resource;
}

NotificationMessageV2::Id NotificationMessageV2::parentCollection() const
{
  return d->parentCollection;
}

NotificationMessageV2::Id NotificationMessageV2::parentDestCollection() const
{
  return d->parentDestCollection;
}

void NotificationMessageV2::setParentCollection( Id parent )
{
  d->parentCollection = parent;
}

void NotificationMessageV2::setParentDestCollection( Id parent )
{
  d->parentDestCollection = parent;
}

void NotificationMessageV2::setDestinationResource( const QByteArray &destResource )
{
  d->destResource = destResource;
}

QByteArray NotificationMessageV2::destinationResource() const
{
  return d->destResource;
}

QSet<QByteArray> NotificationMessageV2::itemParts() const
{
  return d->parts;
}

void NotificationMessageV2::setItemParts( const QSet<QByteArray> &parts )
{
  d->parts = parts;
}

QSet<QByteArray> NotificationMessageV2::addedFlags() const
{
  return d->addedFlags;
}

void NotificationMessageV2::setAddedFlags( const QSet<QByteArray> &addedFlags )
{
  d->addedFlags = addedFlags;
}

QSet<QByteArray> NotificationMessageV2::removedFlags() const
{
  return d->removedFlags;
}

void NotificationMessageV2::setRemovedFlags( const QSet<QByteArray> &removedFlags )
{
  d->removedFlags = removedFlags;
}

QSet<qint64> NotificationMessageV2::addedTags() const
{
  return d->addedTags;
}

void NotificationMessageV2::setAddedTags( const QSet<qint64> &addedTags )
{
  d->addedTags = addedTags;
}

QSet<qint64> NotificationMessageV2::removedTags() const
{
  return d->removedTags;
}

void NotificationMessageV2::setRemovedTags( const QSet<qint64> &removedTags )
{
  d->removedTags = removedTags;
}

QString NotificationMessageV2::toString() const
{
  QString rv;

  switch ( d->type ) {
  case Items:
    rv += QLatin1String( "Items " );
    break;
  case Collections:
    rv += QLatin1String( "Collections " );
    break;
  case Tags:
    rv += QLatin1String( "Tags " );
    break;
  case InvalidType:
    return QLatin1String( "*INVALID TYPE* " );
  }

  QSet<QByteArray> items;
  Q_FOREACH ( const NotificationMessageV2::Entity &item, d->items ) {
    QString itemStr = QString::fromLatin1( "(%1,%2" ).arg( item.id ).arg( item.remoteId );
    if ( !item.remoteRevision.isEmpty() ) {
      itemStr += QString::fromLatin1( ",%1" ).arg( item.remoteRevision );
    }
    if ( !item.mimeType.isEmpty() ) {
      itemStr += QString::fromLatin1( ",%1" ).arg( item.mimeType );
    }
    itemStr += QLatin1String( ")" );
    items << itemStr.toLatin1();
  }
  rv += QLatin1String( "(" ) + QString::fromLatin1( ImapParser::join( items, ", " ) ) + QLatin1String( ")" );

  if ( d->parentDestCollection >= 0 ) {
    rv += QLatin1String( " from " );
  } else {
    rv += QLatin1String( " in " );
  }

  if ( d->parentCollection >= 0 ) {
    rv += QString::fromLatin1( "collection %1 " ).arg( d->parentCollection );
  } else {
    rv += QLatin1String( "unspecified parent collection " );
  }

  switch ( d->operation ) {
  case Add:
    rv += QLatin1String( "added" );
    break;
  case Modify:
    rv += QLatin1String( "modified parts (" );
    rv += QString::fromLatin1( ImapParser::join( d->parts.toList(), ", " ) );
    rv += QLatin1String( ")" );
    break;
  case ModifyFlags:
    rv += QLatin1String( "added flags (" );
    rv += QString::fromLatin1( ImapParser::join( d->addedFlags.toList(), ", " ) );
    rv += QLatin1String( ") " );

    rv += QLatin1String( "removed flags (" );
    rv += QString::fromLatin1( ImapParser::join( d->removedFlags.toList(), ", " ) );
    rv += QLatin1String( ") " );
    break;
  case ModifyTags: {
    rv += QLatin1String( "added tags (" );
    QList<QByteArray> tags;
    Q_FOREACH ( qint64 tagId, d->addedTags ) {
      tags << QByteArray::number( tagId );
    }
    rv += QString::fromLatin1( ImapParser::join( tags, ", " ) );
    rv += QLatin1String( ") " );

    tags.clear();
    Q_FOREACH ( qint64 tagId, d->removedTags ) {
      tags << QByteArray::number( tagId );
    }
    rv += QLatin1String( "removed tags (" );
    rv += QString::fromLatin1( ImapParser::join( tags, ", " ) );
    rv += QLatin1String( ") " );
    break;
  }
  case Move:
    rv += QLatin1String( "moved" );
    break;
  case Remove:
    rv += QLatin1String( "removed" );
    break;
  case Link:
    rv += QLatin1String( "linked" );
    break;
  case Unlink:
    rv += QLatin1String( "unlinked" );
    break;
  case Subscribe:
    rv += QLatin1String( "subscribed" );
    break;
  case Unsubscribe:
    rv += QLatin1String( "unsubscribed" );
    break;
  case InvalidOp:
    return QLatin1String( "*INVALID OPERATION*" );
  }

  if ( d->parentDestCollection >= 0 ) {
    rv += QString::fromLatin1( " to collection %1" ).arg( d->parentDestCollection );
  }

  return rv;
}

QDBusArgument &operator<<( QDBusArgument &arg, const Akonadi::NotificationMessageV2 &msg )
{
  arg.beginStructure();
  arg << msg.sessionId();
  arg << static_cast<int>( msg.type() );
  arg << static_cast<int>( msg.operation() );
  arg << msg.entities().values();
  arg << msg.resource();
  arg << msg.destinationResource();
  arg << msg.parentCollection();
  arg << msg.parentDestCollection();

  QStringList itemParts;
  Q_FOREACH ( const QByteArray &itemPart, msg.itemParts() ) {
    itemParts.append( QString::fromLatin1( itemPart ) );
  }
  arg << itemParts;

  arg << msg.addedFlags().toList();
  arg << msg.removedFlags().toList();

  arg.endStructure();
  return arg;
}

const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2 &msg )
{
  QByteArray ba;
  int i;
  QList<NotificationMessageV2::Entity> items;
  NotificationMessageV2::Id id;
  QString str;
  QStringList strl;
  QList<QByteArray> bal;

  arg.beginStructure();
  arg >> ba;
  msg.setSessionId( ba );
  arg >> i;
  msg.setType( static_cast<NotificationMessageV2::Type>( i ) );
  arg >> i;
  msg.setOperation( static_cast<NotificationMessageV2::Operation>( i ) );
  arg >> items;
  msg.setEntities( items );
  arg >> ba;
  msg.setResource( ba );
  arg >> ba;
  msg.setDestinationResource( ba );
  arg >> id;
  msg.setParentCollection( id );
  arg >> id;
  msg.setParentDestCollection( id );

  arg >> strl;

  QSet<QByteArray> itemParts;
  Q_FOREACH ( const QString &itemPart, strl ) {
    itemParts.insert( itemPart.toLatin1() );
  }
  msg.setItemParts( itemParts );

  arg >> bal;
  msg.setAddedFlags( bal.toSet() );
  arg >> bal;
  msg.setRemovedFlags( bal.toSet() );

  arg.endStructure();
  return arg;
}

QDBusArgument &operator<<( QDBusArgument &arg, const Akonadi::NotificationMessageV2::Entity &item )
{
  arg.beginStructure();
  arg << item.id;
  arg << item.remoteId;
  arg << item.remoteRevision;
  arg << item.mimeType;
  arg.endStructure();

  return arg;
}

const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2::Entity &item )
{
  arg.beginStructure();
  arg >> item.id;
  arg >> item.remoteId;
  arg >> item.remoteRevision;
  arg >> item.mimeType;
  arg.endStructure();

  return arg;
}

QDBusArgument &operator<<( QDBusArgument &arg, Akonadi::NotificationMessageV2::Type type )
{
  arg.beginStructure();
  arg << static_cast<int>( type );
  arg.endStructure();

  return arg;
}

const QDBusArgument &operator>>( const QDBusArgument &arg, Akonadi::NotificationMessageV2::Type &type )
{
  int t;
  arg.beginStructure();
  arg >> t;
  arg.endStructure();
  type = static_cast<NotificationMessageV2::Type>( t );

  return arg;
}

uint qHash( const Akonadi::NotificationMessageV2 &msg )
{
  uint i = 0;
  Q_FOREACH ( const NotificationMessageV2::Entity &item, msg.entities() ) {
    i += item.id;
  }

  return qHash( i + ( msg.type() << 31 ) + ( msg.operation() << 28 ) );
}

QVector<NotificationMessage> NotificationMessageV2::toNotificationV1() const
{
  QVector<NotificationMessage> v1;

  Q_FOREACH ( const Entity &item, d->items ) {
    NotificationMessage msgv1;
    msgv1.setSessionId( d->sessionId );
    msgv1.setUid( item.id );
    msgv1.setRemoteId( item.remoteId );
    msgv1.setMimeType( item.mimeType );
    msgv1.setType( static_cast<NotificationMessage::Type>( d->type ) );
    if ( d->operation == ModifyFlags ) {
      msgv1.setOperation( NotificationMessage::Modify );
    } else {
      msgv1.setOperation( static_cast<NotificationMessage::Operation>( d->operation ) );
    }

    msgv1.setResource( d->resource );
    msgv1.setDestinationResource( d->destResource );
    msgv1.setParentCollection( d->parentCollection );
    msgv1.setParentDestCollection( d->parentDestCollection );

    // Backward compatibility hack
    QSet<QByteArray> parts;
    if ( d->operation == Remove ) {
      QByteArray rr = item.remoteRevision.toLatin1();
      parts << ( rr.isEmpty() ? "1" : rr );
    } else if ( d->operation == ModifyFlags ) {
      parts << "FLAGS";
    } else {
      parts = d->parts;
    }
    msgv1.setItemParts( parts );

    v1 << msgv1;
  }

  return v1;
}

bool NotificationMessageV2::appendAndCompress( NotificationMessageV2::List &list, const NotificationMessageV2 &msg )
{
  return NotificationMessageHelpers::appendAndCompressImpl<NotificationMessageV2::List, NotificationMessageV2>( list, msg );
}

bool NotificationMessageV2::appendAndCompress( QList<NotificationMessageV2> &list, const NotificationMessageV2 &msg )
{
  return NotificationMessageHelpers::appendAndCompressImpl<QList<NotificationMessageV2>, NotificationMessageV2>( list, msg );
}

QDebug operator<<( QDebug dbg, const NotificationMessageV2::Entity &entity)
{
  dbg.nospace() << "(ID: " << entity.id;
  if (!entity.remoteId.isEmpty()) {
    dbg.nospace() << " RID: " << entity.remoteId;
  }
  if (!entity.remoteRevision.isEmpty()) {
     dbg.nospace() << " RREV: " << entity.remoteRevision;
  }
  if (!entity.mimeType.isEmpty()) {
     dbg.nospace() << " MimeType: " << entity.mimeType;
  }
  dbg.nospace() << ")";
  return dbg;
}
