/*
    Copyright (c) 2006 - 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.
*/

//@cond PRIVATE

#include "collectionmodel_p.h"
#include "collectionmodel.h"
#include "collectionutils_p.h"

#include "collectionfetchjob.h"
#include "collectionstatistics.h"
#include "collectionstatisticsjob.h"
#include "monitor.h"
#include "session.h"

#include <kdebug.h>
#include <kjob.h>
#include <kiconloader.h>

#include <QCoreApplication>
#include <QtCore/QTimer>
#include "collectionfetchscope.h"

using namespace Akonadi;

void CollectionModelPrivate::collectionRemoved( const Akonadi::Collection &collection )
{
  Q_Q( CollectionModel );
  QModelIndex colIndex = indexForId( collection.id() );
  if ( colIndex.isValid() ) {
    QModelIndex parentIndex = q->parent( colIndex );
    // collection is still somewhere in the hierarchy
    removeRowFromModel( colIndex.row(), parentIndex );
  } else {
    if ( collections.contains( collection.id() ) ) {
      // collection is orphan, ie. the parent has been removed already
      collections.remove( collection.id() );
      childCollections.remove( collection.id() );
    }
  }
}

void CollectionModelPrivate::collectionChanged( const Akonadi::Collection &collection )
{
  Q_Q( CollectionModel );
  // What kind of change is it ?
  Collection::Id oldParentId = collections.value( collection.id() ).parentCollection().id();
  Collection::Id newParentId = collection.parentCollection().id();
  if ( newParentId !=  oldParentId && oldParentId >= 0 ) { // It's a move
    removeRowFromModel( indexForId( collections[ collection.id() ].id() ).row(), indexForId( oldParentId ) );
    Collection newParent;
    if ( newParentId == Collection::root().id() ) {
      newParent = Collection::root();
    } else {
      newParent = collections.value( newParentId );
    }
    CollectionFetchJob *job = new CollectionFetchJob( newParent, CollectionFetchJob::Recursive, session );
    job->fetchScope().setIncludeUnsubscribed( unsubscribed );
    job->fetchScope().setIncludeStatistics( fetchStatistics );
    q->connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)),
                q, SLOT(collectionsChanged(Akonadi::Collection::List)) );
    q->connect( job, SIGNAL(result(KJob*)),
                 q, SLOT(listDone(KJob*)) );

  } else { // It's a simple change
    CollectionFetchJob *job = new CollectionFetchJob( collection, CollectionFetchJob::Base, session );
    job->fetchScope().setIncludeUnsubscribed( unsubscribed );
    job->fetchScope().setIncludeStatistics( fetchStatistics );
    q->connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)),
                q, SLOT(collectionsChanged(Akonadi::Collection::List)) );
    q->connect( job, SIGNAL(result(KJob*)),
                 q, SLOT(listDone(KJob*)) );
  }

}

void CollectionModelPrivate::updateDone( KJob *job )
{
  if ( job->error() ) {
    // TODO: handle job errors
    kWarning() << "Job error:" << job->errorString();
  } else {
    CollectionStatisticsJob *csjob = static_cast<CollectionStatisticsJob*>( job );
    Collection result = csjob->collection();
    collectionStatisticsChanged( result.id(), csjob->statistics() );
  }
}

void CollectionModelPrivate::collectionStatisticsChanged( Collection::Id collection,
                               const Akonadi::CollectionStatistics &statistics )
{
  Q_Q( CollectionModel );

  if ( !collections.contains( collection ) ) {
    kWarning() << "Got statistics response for non-existing collection:" << collection;
  } else {
    collections[ collection ].setStatistics( statistics );

    Collection col = collections.value( collection );
    QModelIndex startIndex = indexForId( col.id() );
    QModelIndex endIndex = indexForId( col.id(), q->columnCount( q->parent( startIndex ) ) - 1 );
    emit q->dataChanged( startIndex, endIndex );
  }
}

void CollectionModelPrivate::listDone( KJob *job )
{
  if ( job->error() ) {
    kWarning() << "Job error: " << job->errorString() << endl;
  }
}

void CollectionModelPrivate::editDone( KJob * job )
{
  if ( job->error() ) {
    kWarning() << "Edit failed: " << job->errorString();
  }
}

void CollectionModelPrivate::dropResult(KJob * job)
{
  if ( job->error() ) {
    kWarning() << "Paste failed:" << job->errorString();
    // TODO: error handling
  }
}

void CollectionModelPrivate::collectionsChanged( const Collection::List &cols )
{
  Q_Q( CollectionModel );

  foreach ( Collection col, cols ) { //krazy:exclude=foreach non-const is needed here
    if ( collections.contains( col.id() ) ) {
      // If the collection is already known to the model, we simply update it...
      col.setStatistics( collections.value( col.id() ).statistics() );
      collections[ col.id() ] = col;
      QModelIndex startIndex = indexForId( col.id() );
      QModelIndex endIndex = indexForId( col.id(), q->columnCount( q->parent( startIndex ) ) - 1 );
      emit q->dataChanged( startIndex, endIndex );
      continue;
    }
    // ... otherwise we add it to the set of collections we need to handle.
    m_newChildCollections[ col.parentCollection().id() ].append( col.id() );
    m_newCollections.insert( col.id(), col );
  }

  // Handle the collections in m_newChildCollections. If the collections
  // parent is already in the model, the collection can be added to the model.
  // Otherwise it is persisted until it has a valid parent in the model.
  int currentSize = m_newChildCollections.size();
  int lastSize = -1;

  while ( currentSize > 0 ) {
    lastSize = currentSize;

    QMutableHashIterator< Collection::Id, QVector< Collection::Id > > i( m_newChildCollections );
    while ( i.hasNext() ) {
      i.next();

      // the key is the parent of new collections. It may itself also be new,
      // but that will be handled later.
      Collection::Id colId = i.key();

      QVector< Collection::Id > newChildCols = i.value();
      int newChildCount = newChildCols.size();
//       if ( newChildCount == 0 )
//       {
//         // Sanity check.
//         kDebug() << "No new child collections have been added to the collection:" << colId;
//         i.remove();
//         currentSize--;
//         break;
//       }

      if ( collections.contains( colId ) || colId == Collection::root().id() ) {
        QModelIndex parentIndex = indexForId( colId );
        int currentChildCount = childCollections.value( colId ).size();

        q->beginInsertRows( parentIndex,
                            currentChildCount,      // Start index is at the end of existing collections.
                            currentChildCount + newChildCount - 1 ); // End index is the result of the insertion.

        foreach ( Collection::Id id, newChildCols ) {
          Collection c = m_newCollections.take( id );
          collections.insert( id, c );
        }

        childCollections[ colId ] << newChildCols;
        q->endInsertRows();
        i.remove();
        currentSize--;
        break;
      }
    }

    // We iterated through once without adding any more collections to the model.
    if ( currentSize == lastSize ) {
      // The remaining collections in the list do not have a valid parent in the model yet. They
      // might arrive in the next batch from the monitor, so they're still in m_newCollections
      // and m_newChildCollections.
      kDebug() << "Some collections did not have a parent in the model yet!";
      break;
    }
  }
}

QModelIndex CollectionModelPrivate::indexForId( Collection::Id id, int column ) const
{
  Q_Q( const CollectionModel );
  if ( !collections.contains( id ) ) {
    return QModelIndex();
  }

  Collection::Id parentId = collections.value( id ).parentCollection().id();
  // check if parent still exist or if this is an orphan collection
  if ( parentId != Collection::root().id() && !collections.contains( parentId ) ) {
    return QModelIndex();
  }

  QVector<Collection::Id> list = childCollections.value( parentId );
  int row = list.indexOf( id );

  if ( row >= 0 ) {
    return q->createIndex( row, column, reinterpret_cast<void*>( collections.value( list.at( row ) ).id() ) );
  }
  return QModelIndex();
}

bool CollectionModelPrivate::removeRowFromModel( int row, const QModelIndex & parent )
{
  Q_Q( CollectionModel );
  QVector<Collection::Id> list;
  Collection parentCol;
  if ( parent.isValid() ) {
    parentCol = collections.value( parent.internalId() );
    Q_ASSERT( parentCol.id() == parent.internalId() );
    list = childCollections.value( parentCol.id() );
  } else {
    parentCol = Collection::root();
    list = childCollections.value( Collection::root().id() );
  }
  if ( row < 0 || row  >= list.size() ) {
    kWarning() << "Index out of bounds:" << row << " parent:" << parentCol.id();
    return false;
  }

  q->beginRemoveRows( parent, row, row );
  const Collection::Id delColId = list[row];
  list.remove( row );
  foreach ( Collection::Id childColId, childCollections[ delColId ] ) {
    collections.remove( childColId );
  }
  collections.remove( delColId );
  childCollections.remove( delColId ); // remove children of deleted collection
  childCollections.insert( parentCol.id(), list ); // update children of parent
  q->endRemoveRows();

  return true;
}

bool CollectionModelPrivate::supportsContentType(const QModelIndex & index, const QStringList & contentTypes)
{
  if ( !index.isValid() ) {
    return false;
  }
  Collection col = collections.value( index.internalId() );
  Q_ASSERT( col.isValid() );
  QStringList ct = col.contentMimeTypes();
  foreach ( const QString &a, ct ) {
    if ( contentTypes.contains( a ) ) {
      return true;
    }
  }
  return false;
}

void CollectionModelPrivate::init()
{
  Q_Q( CollectionModel );

  session = new Session( QCoreApplication::instance()->applicationName().toUtf8()
      + QByteArray( "-CollectionModel-" ) + QByteArray::number( qrand() ), q );
  QTimer::singleShot( 0, q, SLOT(startFirstListJob()) );

  // monitor collection changes
  monitor = new Monitor();
  monitor->setCollectionMonitored( Collection::root() );
  monitor->fetchCollection( true );

  // ### Hack to get the kmail resource folder icons
  KIconLoader::global()->addAppDir( QLatin1String( "kmail" ) );
  KIconLoader::global()->addAppDir( QLatin1String( "kdepim" ) );

  // monitor collection changes
  q->connect( monitor, SIGNAL(collectionChanged(Akonadi::Collection)),
              q, SLOT(collectionChanged(Akonadi::Collection)) );
  q->connect( monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)),
              q, SLOT(collectionChanged(Akonadi::Collection)) );
  q->connect( monitor, SIGNAL(collectionRemoved(Akonadi::Collection)),
              q, SLOT(collectionRemoved(Akonadi::Collection)) );
  q->connect( monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)),
              q, SLOT(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)) );
}

void CollectionModelPrivate::startFirstListJob()
{
  Q_Q( CollectionModel );

  // start a list job
  CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, session );
  job->fetchScope().setIncludeUnsubscribed( unsubscribed );
  job->fetchScope().setIncludeStatistics( fetchStatistics );
  q->connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)),
              q, SLOT(collectionsChanged(Akonadi::Collection::List)) );
  q->connect( job, SIGNAL(result(KJob*)), q, SLOT(listDone(KJob*)) );
}

//@endcond
