/*
    Copyright (c) 2006 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 "item.h"
#include "item_p.h"
#include "itemserializer_p.h"
#include "protocol_p.h"

#include <kurl.h>
#include <kdebug.h>

#include <QtCore/QStringList>
#include <QtCore/QReadWriteLock>

#include <algorithm>
#include <map>
#include <utility>

using namespace Akonadi;
using namespace boost;

namespace {

struct nodelete {
    template <typename T>
    void operator()( T * ) {}
};

struct ByTypeId {
    typedef bool result_type;
    bool operator()( const boost::shared_ptr<PayloadBase> & lhs, const boost::shared_ptr<PayloadBase> & rhs ) const {
        return strcmp( lhs->typeName(), rhs->typeName() ) < 0 ;
    }
};

} // anon namespace

typedef QHash< QString, std::map< boost::shared_ptr<PayloadBase>, std::pair<int,int>, ByTypeId > > LegacyMap;
Q_GLOBAL_STATIC( LegacyMap, typeInfoToMetaTypeIdMap )
Q_GLOBAL_STATIC_WITH_ARGS( QReadWriteLock, legacyMapLock, ( QReadWriteLock::Recursive ) )

void Item::addToLegacyMappingImpl( const QString & mimeType, int spid, int mtid, std::auto_ptr<PayloadBase> p ) {
    if ( !p.get() )
        return;
    const boost::shared_ptr<PayloadBase> sp( p );
    const QWriteLocker locker( legacyMapLock() );
    std::pair<int,int> & item = (*typeInfoToMetaTypeIdMap())[mimeType][sp];
    item.first = spid;
    item.second = mtid;
}

namespace {
    class MyReadLocker {
    public:
        explicit MyReadLocker( QReadWriteLock * rwl ) : rwl( rwl ), locked( false ) { if ( rwl ) rwl->lockForRead(); locked = true; }
        ~MyReadLocker() { if ( rwl && locked ) rwl->unlock(); }

        template <typename T>
        shared_ptr<T> makeUnlockingPointer( T * t ) {
            if ( t ) {
                // the bind() doesn't throw, so if shared_ptr
                // construction line below, or anything else after it,
                // throws, we're unlocked. Mark us as such:
                locked = false;
                const shared_ptr<T> result( t, bind( &QReadWriteLock::unlock, rwl ) );
                // from now on, the shared_ptr is responsible for unlocking
                return result;
            } else {
                return shared_ptr<T>();
            }
        }
    private:
        QReadWriteLock * const rwl;
        bool locked;
    };
}

static shared_ptr<const std::pair<int,int> > lookupLegacyMapping( const QString & mimeType, PayloadBase * p ) {
    MyReadLocker locker( legacyMapLock() );
    const LegacyMap::const_iterator hit = typeInfoToMetaTypeIdMap()->constFind( mimeType );
    if ( hit == typeInfoToMetaTypeIdMap()->constEnd() )
        return shared_ptr<const std::pair<int,int> >();
    const boost::shared_ptr<PayloadBase> sp( p, nodelete() );
    const LegacyMap::mapped_type::const_iterator it = hit->find( sp );
    if ( it == hit->end() )
        return shared_ptr<const std::pair<int,int> >();
    
    return locker.makeUnlockingPointer( &it->second );
}

// Change to something != RFC822 as soon as the server supports it
const char* Item::FullPayload = "RFC822";

Item::Item()
  : Entity( new ItemPrivate )
{
}

Item::Item( Id id )
  : Entity( new ItemPrivate( id ) )
{
}

Item::Item( const QString & mimeType )
  : Entity( new ItemPrivate )
{
  d_func()->mMimeType = mimeType;
}

Item::Item( const Item &other )
  : Entity( other )
{
}

Item::~Item()
{
}

Item::Flags Item::flags() const
{
  return d_func()->mFlags;
}

void Item::setFlag( const QByteArray & name )
{
  Q_D( Item );
  d->mFlags.insert( name );
  if ( !d->mFlagsOverwritten ) {
    if ( d->mDeletedFlags.contains( name ) )
      d->mDeletedFlags.remove( name );
    else
      d->mAddedFlags.insert( name );
  }
}

void Item::clearFlag( const QByteArray & name )
{
  Q_D( Item );
  d->mFlags.remove( name );
  if ( !d->mFlagsOverwritten ) {
    if ( d->mAddedFlags.contains( name ) )
      d->mAddedFlags.remove( name );
    else
      d->mDeletedFlags.insert( name );
  }
}

void Item::setFlags( const Flags &flags )
{
  Q_D( Item );
  d->mFlags = flags;
  d->mFlagsOverwritten = true;
}

void Item::clearFlags()
{
  Q_D( Item );
  d->mFlags.clear();
  d->mFlagsOverwritten = true;
}

QDateTime Item::modificationTime() const
{
  return d_func()->mModificationTime;
}

void Item::setModificationTime( const QDateTime &datetime )
{
  d_func()->mModificationTime = datetime;
}

bool Item::hasFlag( const QByteArray & name ) const
{
  return d_func()->mFlags.contains( name );
}

QSet<QByteArray> Item::loadedPayloadParts() const
{
  return ItemSerializer::parts( *this );
}

QByteArray Item::payloadData() const
{
  int version = 0;
  QByteArray data;
  ItemSerializer::serialize( *this, FullPayload, data, version );
  return data;
}

void Item::setPayloadFromData( const QByteArray &data )
{
  ItemSerializer::deserialize( *this, FullPayload, data, 0, false );
}

void Item::clearPayload()
{
  d_func()->mClearPayload = true;
}

int Item::revision() const
{
  return d_func()->mRevision;
}

void Item::setRevision( int rev )
{
  d_func()->mRevision = rev;
}

Entity::Id Item::storageCollectionId() const
{
  return d_func()->mCollectionId;
}

void Item::setStorageCollectionId( Entity::Id collectionId )
{
  d_func()->mCollectionId = collectionId;
}

QString Item::mimeType() const
{
  return d_func()->mMimeType;
}

void Item::setSize( qint64 size )
{
  Q_D( Item );
  d->mSize = size;
  d->mSizeChanged = true;
}

qint64 Item::size() const
{
  return d_func()->mSize;
}

void Item::setMimeType( const QString & mimeType )
{
  d_func()->mMimeType = mimeType;
}

bool Item::hasPayload() const
{
  return d_func()->hasMetaTypeId( -1 );
}

KUrl Item::url( UrlType type ) const
{
  KUrl url;
  url.setProtocol( QString::fromLatin1( "akonadi" ) );
  url.addQueryItem( QLatin1String( "item" ), QString::number( id() ) );

  if ( type == UrlWithMimeType )
    url.addQueryItem( QLatin1String( "type" ), mimeType() );

  return url;
}

Item Item::fromUrl( const KUrl &url )
{
  if ( url.protocol() != QLatin1String( "akonadi" ) )
    return Item();

  const QString itemStr = url.queryItem( QLatin1String( "item" ) );
  bool ok = false;
  Item::Id itemId = itemStr.toLongLong( &ok );
  if ( !ok )
    return Item();

  return Item( itemId );
}

namespace {
    class Dummy {};
}

Q_GLOBAL_STATIC( Payload<Dummy>, dummyPayload )

PayloadBase* Item::payloadBase() const
{
  Q_D( const Item );
  d->tryEnsureLegacyPayload();
  if ( d->mLegacyPayload )
    return d->mLegacyPayload.get();
  else
    return dummyPayload();
}

void ItemPrivate::tryEnsureLegacyPayload() const
{
  if ( !mLegacyPayload )
    for ( PayloadContainer::const_iterator it = mPayloads.begin(), end = mPayloads.end() ; it != end ; ++it )
      if ( lookupLegacyMapping( mMimeType, it->payload.get() ) )
          mLegacyPayload = it->payload; // clones
}

PayloadBase* Item::payloadBaseV2( int spid, int mtid ) const
{
  return d_func()->payloadBaseImpl( spid, mtid );
}

namespace {
    class ConversionGuard {
        const bool old;
        bool & b;
    public:
        explicit ConversionGuard( bool & b )
            : old( b ), b( b )
        {
            b = true;
        }
        ~ConversionGuard() {
            b = old;
        }
    };
}
            

bool Item::ensureMetaTypeId( int mtid ) const
{
  Q_D( const Item );
  // 0. Nothing there - nothing to convert from, either
  if ( d->mPayloads.empty() )
    return false;

  // 1. Look whether we already have one:
  if ( d->hasMetaTypeId( mtid ) )
    return true;

  // recursion detection (shouldn't trigger, but does if the
  // serialiser plugins are acting funky):
  if ( d->mConversionInProgress )
      return false;

  // 2. Try to create one by conversion from a different representation:
  try {
    const ConversionGuard guard( d->mConversionInProgress );
    Item converted = ItemSerializer::convert( *this, mtid );
    return d->movePayloadFrom( converted.d_func(), mtid );
  } catch ( const std::exception & e ) {
    kDebug() << "conversion threw:" << e.what();
    return false;
  } catch ( ... ) {
    kDebug() << "conversion threw something not derived from std::exception: fix the program!";
    return false;
  }
}

static QString format_type( int spid, int mtid ) {
  return QString::fromLatin1( "sp(%1)<%2>" )
      .arg( spid ).arg( QLatin1String( QMetaType::typeName( mtid ) ) );
}

static QString format_types( const PayloadContainer & c ) {
  QStringList result;
  for ( PayloadContainer::const_iterator it = c.begin(), end = c.end() ; it != end ; ++it )
    result.push_back( format_type( it->sharedPointerId, it->metaTypeId ) );
  return result.join( QLatin1String(", ") );
}

#if 0
QString Item::payloadExceptionText( int spid, int mtid ) const
{
  Q_D( const Item );
  if ( d->mPayloads.empty() )
    return QLatin1String( "No payload set" );
  else
    return QString::fromLatin1( "Wrong payload type (requested: %1; present: %2" )
        .arg( format_type( spid, mtid ), format_types( d->mPayloads ) );
}
#else
void Item::throwPayloadException( int spid, int mtid ) const
{
  Q_D( const Item );
  if ( d->mPayloads.empty() )
    throw PayloadException( "No payload set" );
  else
    throw PayloadException( QString::fromLatin1( "Wrong payload type (requested: %1; present: %2" )
                            .arg( format_type( spid, mtid ), format_types( d->mPayloads ) ) );
}
#endif

void Item::setPayloadBase( PayloadBase* p )
{
  d_func()->setLegacyPayloadBaseImpl( std::auto_ptr<PayloadBase>( p ) );
}

void ItemPrivate::setLegacyPayloadBaseImpl( std::auto_ptr<PayloadBase> p )
{
  if ( const shared_ptr<const std::pair<int,int> > pair = lookupLegacyMapping( mMimeType, p.get() ) ) {
    std::auto_ptr<PayloadBase> clone;
    if ( p.get() )
      clone.reset( p->clone() );
    setPayloadBaseImpl( pair->first, pair->second, p, false );
    mLegacyPayload.reset( clone.release() );
  } else {
    mPayloads.clear();
    mLegacyPayload.reset( p.release() );
  }
}

void Item::setPayloadBaseV2( int spid, int mtid, std::auto_ptr<PayloadBase> p )
{
  d_func()->setPayloadBaseImpl( spid, mtid, p, false );
}

void Item::addPayloadBaseVariant( int spid, int mtid, std::auto_ptr<PayloadBase> p ) const
{
  d_func()->setPayloadBaseImpl( spid, mtid, p, true );
}

QSet<QByteArray> Item::availablePayloadParts() const
{
  return ItemSerializer::availableParts( *this );
}

QVector<int> Item::availablePayloadMetaTypeIds() const
{
  QVector<int> result;
  Q_D( const Item );
  result.reserve( d->mPayloads.size() );
  // Stable Insertion Sort - N is typically _very_ low (1 or 2).
  for ( PayloadContainer::const_iterator it = d->mPayloads.begin(), end = d->mPayloads.end() ; it != end ; ++it )
      result.insert( std::upper_bound( result.begin(), result.end(), it->metaTypeId ), it->metaTypeId );
  return result;
}

void Item::apply( const Item &other )
{
  if ( mimeType() != other.mimeType() || id() != other.id() ) {
    kDebug() << "mimeType() = " << mimeType() << "; other.mimeType() = " << other.mimeType();
    kDebug() << "id() = " << id() << "; other.id() = " << other.id();
    Q_ASSERT_X( false, "Item::apply", "mimetype or id missmatch" );
  }

  setRemoteId( other.remoteId() );
  setRevision( other.revision() );
  setFlags( other.flags() );
  setModificationTime( other.modificationTime() );
  setSize( other.size() );
  setParentCollection( other.parentCollection() );
  setStorageCollectionId( other.storageCollectionId() );
  setRemoteId( other.remoteId() );

  QList<QByteArray> attrs;
  foreach ( Attribute *attribute, other.attributes() )
  {
    addAttribute( attribute->clone() );
    attrs.append( attribute->type() );
  }

  QMutableHashIterator<QByteArray, Attribute*> it( d_ptr->mAttributes );
  while ( it.hasNext() ) {
    it.next();
    if ( !attrs.contains( it.key() ) ) {
      delete it.value();
      it.remove();
    }
  }

  ItemSerializer::apply( *this, other );
  d_func()->resetChangeLog();
}

AKONADI_DEFINE_PRIVATE( Item )
