/*
 * Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
 * Copyright (c) 2010 Tobias Koenig <tokoe@kdab.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "aclmanager.h"

#include "aclentrydialog_p.h"
#include "aclutils_p.h"
#include "imapaclattribute.h"
#include "imapsettings.h"
#include "mailutil.h"

#include <Akonadi/Collection>
#include <Akonadi/CollectionFetchJob>
#include <Akonadi/CollectionModifyJob>
#include <Akonadi/Contact/ContactGroupExpandJob>
#include <Akonadi/Contact/ContactGroupSearchJob>

#include <KPIMUtils/Email>

#include <KLocale>
#include <KMessageBox>

#include <QAbstractListModel>
#include <QAction>
#include <QItemSelectionModel>

using namespace MailCommon;

class AclModel : public QAbstractListModel
{
  public:
    enum Role {
      UserIdRole = Qt::UserRole + 1,
      PermissionsRole,
      PermissionsTextRole
    };

    AclModel( QObject *parent = 0 )
      : QAbstractListModel( parent )
    {
    }

    virtual QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const
    {
      if ( index.row() < 0 || index.row() >= mRights.count() ) {
        return QVariant();
      }

      const QPair<QByteArray, KIMAP::Acl::Rights> right = mRights.at( index.row() );
      switch ( role ) {
      case Qt::DisplayRole:
        return QString::fromLatin1( "%1: %2" ).
                 arg( QString::fromLatin1( right.first ) ).
                 arg( AclUtils::permissionsToUserString( right.second ) );
        break;
      case UserIdRole:
        return QString::fromLatin1( right.first );
        break;
      case PermissionsRole:
        return QVariant( static_cast<int>( right.second ) );
        break;
      case PermissionsTextRole:
        return AclUtils::permissionsToUserString( right.second );
        break;
      default:
        return QVariant();
        break;
      }

      return QVariant();
    }

    virtual bool setData( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole )
    {
      if ( index.row() < 0 || index.row() >= mRights.count() ) {
        return false;
      }

      QPair<QByteArray, KIMAP::Acl::Rights> &right = mRights[ index.row() ];
      switch ( role ) {
      case UserIdRole:
        right.first = value.toByteArray();
        emit dataChanged( index, index );
        return true;
        break;
      case PermissionsRole:
        right.second = static_cast<KIMAP::Acl::Rights>( value.toInt() );
        emit dataChanged( index, index );
        return true;
        break;
      default:
        return false;
        break;
      }

      return false;
    }

    virtual int rowCount( const QModelIndex &parent = QModelIndex() ) const
    {
      if ( parent.isValid() ) {
        return 0;
      } else {
        return mRights.count();
      }
    }

    void setRights( const QMap<QByteArray, KIMAP::Acl::Rights> &rights )
    {
      mRights.clear();

      QMapIterator<QByteArray, KIMAP::Acl::Rights> it( rights );
      while ( it.hasNext() ) {
        it.next();
        mRights.append( qMakePair( it.key(), it.value() ) );
      }

      reset();
    }

    QMap<QByteArray, KIMAP::Acl::Rights> rights() const
    {
      QMap<QByteArray, KIMAP::Acl::Rights> result;

      typedef QPair<QByteArray, KIMAP::Acl::Rights> RightPair;
      foreach ( const RightPair &right, mRights ) {
        result.insert( right.first, right.second );
      }

      return result;
    }

  protected:
    virtual bool insertRows( int row, int count, const QModelIndex &parent = QModelIndex() )
    {
      beginInsertRows( parent, row, row + count - 1 );
      for ( int i = 0; i < count; ++i ) {
        mRights.insert( row, qMakePair( QByteArray(), KIMAP::Acl::Rights() ) );
      }
      endInsertRows();

      return true;
    }

    virtual bool removeRows( int row, int count, const QModelIndex &parent = QModelIndex() )
    {
      beginRemoveRows( parent, row, row + count - 1 );
      for ( int i = 0; i < count; ++i ) {
        mRights.remove( row, count );
      }
      endRemoveRows();

      return true;
    }

  private:
    QVector<QPair<QByteArray, KIMAP::Acl::Rights> > mRights;
};

class MailCommon::AclManager::Private
{
  public:
    Private( AclManager *qq )
      : q( qq ),
        mChanged( false )
    {
      mAddAction = new QAction( i18n( "Add Entry..." ), q );
      q->connect( mAddAction, SIGNAL(triggered(bool)),
                  q, SLOT(addAcl()) );

      mEditAction = new QAction( i18n( "Edit Entry..." ), q );
      mEditAction->setEnabled( false );
      q->connect( mEditAction, SIGNAL(triggered(bool)),
                  q, SLOT(editAcl()) );

      mDeleteAction = new QAction( i18n( "Remove Entry" ), q );
      mDeleteAction->setEnabled( false );
      q->connect( mDeleteAction, SIGNAL(triggered(bool)),
                  q, SLOT(deleteAcl()) );

      mModel = new AclModel( q );

      mSelectionModel = new QItemSelectionModel( mModel );
      q->connect( mSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
                  q, SLOT(selectionChanged()) );
    }

    ~Private()
    {
    }

    void selectionChanged()
    {
      const bool itemSelected = !mSelectionModel->selectedIndexes().isEmpty();

      bool canAdmin = ( mUserRights & KIMAP::Acl::Admin );

      bool canAdminThisItem = canAdmin;
      if ( canAdmin && itemSelected ) {
        const QModelIndex index = mSelectionModel->selectedIndexes().first();
        const QString userId = index.data( AclModel::UserIdRole ).toString();
        const KIMAP::Acl::Rights rights =
          static_cast<KIMAP::Acl::Rights>( index.data( AclModel::PermissionsRole ).toInt() );

        // Don't allow users to remove their own admin permissions - there's no way back
        if ( mImapUserName == userId && ( rights & KIMAP::Acl::Admin ) ) {
          canAdminThisItem = false;
        }
      }

      mAddAction->setEnabled( canAdmin );
      mEditAction->setEnabled( itemSelected && canAdminThisItem );
      mDeleteAction->setEnabled( itemSelected && canAdminThisItem );
    }

    void addAcl()
    {
      AclEntryDialog dlg;
      dlg.setCaption( i18n( "Add ACL" ) );

      if ( !dlg.exec() ) {
        return;
      }

      if ( mModel->insertRow( mModel->rowCount() ) ) {
        const QModelIndex index = mModel->index( mModel->rowCount() - 1, 0 );
        mModel->setData( index, dlg.userId(), AclModel::UserIdRole );
        mModel->setData( index, static_cast<int>( dlg.permissions() ), AclModel::PermissionsRole );

        mChanged = true;
      }
    }

    void editAcl()
    {
      const QModelIndex index = mSelectionModel->selectedIndexes().first();
      const QString userId = index.data( AclModel::UserIdRole ).toString();
      const KIMAP::Acl::Rights permissions =
        static_cast<KIMAP::Acl::Rights>( index.data( AclModel::PermissionsRole ).toInt() );

      AclEntryDialog dlg;
      dlg.setCaption( i18n( "Edit ACL" ) );
      dlg.setUserId( userId );
      dlg.setPermissions( permissions );

      if ( !dlg.exec() ) {
        return;
      }

      mModel->setData( index, dlg.userId(), AclModel::UserIdRole );
      mModel->setData( index, static_cast<int>( dlg.permissions() ), AclModel::PermissionsRole );
      mChanged = true;
    }

    void deleteAcl()
    {
      const QModelIndex index = mSelectionModel->selectedIndexes().first();
      const QString userId = index.data( AclModel::UserIdRole ).toString();

      if ( mImapUserName == userId ) {
        if ( KMessageBox::Cancel == KMessageBox::warningContinueCancel(
               0,
               i18n( "Do you really want to remove your own permissions for this folder? "
                     "You will not be able to access it afterwards." ),
               i18n( "Remove" ) ) )
          return;
      }

      mModel->removeRow( index.row(), QModelIndex() );
      mChanged = true;
    }

    /**
     * We call this method if our first try to get the ACLs for the user fails.
     * That's the case if the ACLs use a different user id than the login name.
     *
     * Examples:
     *   login: testuser                acls: testuser@mydomain.org
     *   login: testuser@mydomain.org   acls: testuser
     */
    static QString guessUserName( const QString &loginName, const QString &serverName )
    {
      if ( loginName.contains( QLatin1Char( '@' ) ) ) {
        // strip of the domain part and use user name only
        return loginName.left( loginName.indexOf( QLatin1Char( '@' ) ) );
      } else {
        int pos = serverName.lastIndexOf( QLatin1Char( '.' ) );
        if ( pos == -1 ) { // no qualified domain name, only hostname
          return QString::fromLatin1( "%1@%2" ).arg( loginName ).arg( serverName );
        }

        pos = serverName.lastIndexOf( QLatin1Char( '.' ), pos - 1 );
        if ( pos == -1 ) { // a simple domain name e.g. mydomain.org
          return QString::fromLatin1( "%1@%2" ).arg( loginName ).arg( serverName );
        } else {
          return QString::fromLatin1( "%1@%2" ).arg( loginName ).arg( serverName.mid( pos + 1 ) );
        }
      }
    }

    void setCollection( const Akonadi::Collection &collection )
    {
      mCollection = collection;
      mChanged = false;

      const MailCommon::ImapAclAttribute *attribute =
        collection.attribute<MailCommon::ImapAclAttribute>();
      const QMap<QByteArray, KIMAP::Acl::Rights> rights = attribute->rights();

      OrgKdeAkonadiImapSettingsInterface *imapSettingsInterface =
        MailCommon::Util::createImapSettingsInterface( collection.resource() );

      QString loginName;
      QString serverName;
      if ( imapSettingsInterface->isValid() ) {
        QDBusReply<QString> reply = imapSettingsInterface->userName();
        if ( reply.isValid() ) {
          loginName = reply;
        }

        reply = imapSettingsInterface->imapServer();
        if ( reply.isValid() ) {
          serverName = reply;
        }
      }

      delete imapSettingsInterface;

      mImapUserName = loginName;
      if ( !rights.contains( loginName.toUtf8() ) ) {
        const QString guessedUserName = guessUserName( loginName, serverName );
        if ( rights.contains( guessedUserName.toUtf8() ) ) {
          mImapUserName = guessedUserName;
        }
      }

      mUserRights = rights[ mImapUserName.toUtf8() ];

      mModel->setRights( rights );
      selectionChanged();
    }

    AclManager *q;
    AclModel *mModel;
    QItemSelectionModel *mSelectionModel;
    QAction *mAddAction;
    QAction *mEditAction;
    QAction *mDeleteAction;

    Akonadi::Collection mCollection;
    QString mImapUserName;
    KIMAP::Acl::Rights mUserRights;
    bool mChanged;
};

AclManager::AclManager( QObject *parent )
  : QObject( parent ), d( new Private( this ) )
{
}

AclManager::~AclManager()
{
  delete d;
}

void AclManager::setCollection( const Akonadi::Collection &collection )
{
  d->setCollection( collection );
  emit collectionChanged( d->mCollection );
}

Akonadi::Collection AclManager::collection() const
{
  return d->mCollection;
}

QAbstractItemModel *AclManager::model() const
{
  return d->mModel;
}

QItemSelectionModel *AclManager::selectionModel() const
{
  return d->mSelectionModel;
}

QAction *AclManager::addAction() const
{
  return d->mAddAction;
}

QAction *AclManager::editAction() const
{
  return d->mEditAction;
}

QAction *AclManager::deleteAction() const
{
  return d->mDeleteAction;
}

void AclManager::save()
{
  if ( !d->mCollection.isValid() || !d->mChanged ) {
    return;
  }

  // refresh the collection, it might be outdated in the meantime
  Akonadi::CollectionFetchJob *job =
    new Akonadi::CollectionFetchJob( d->mCollection, Akonadi::CollectionFetchJob::Base );
  if ( !job->exec() ) {
    return;
  }

  if ( job->collections().isEmpty() ) {
    return;
  }

  d->mCollection = job->collections().first();

  d->mChanged = false;

  MailCommon::ImapAclAttribute *attribute =
    d->mCollection.attribute<MailCommon::ImapAclAttribute>();

  QMap<QByteArray, KIMAP::Acl::Rights> newRights;

  const QMap<QByteArray, KIMAP::Acl::Rights> rights = d->mModel->rights();
  QMapIterator<QByteArray, KIMAP::Acl::Rights> it( rights );
  while ( it.hasNext() ) {
    it.next();

    // we can use job->exec() here, it is not a hot path
    Akonadi::ContactGroupSearchJob *searchJob = new Akonadi::ContactGroupSearchJob( this );
    searchJob->setQuery( Akonadi::ContactGroupSearchJob::Name, it.key() );
    searchJob->setLimit( 1 );
    if ( !searchJob->exec() ) {
      continue;
    }

    if ( !searchJob->contactGroups().isEmpty() ) { // it has been a distribution list
      Akonadi::ContactGroupExpandJob *expandJob =
        new Akonadi::ContactGroupExpandJob( searchJob->contactGroups().first(), this );
      if ( expandJob->exec() ) {
        foreach ( const KABC::Addressee &contact, expandJob->contacts() ) {
          const QByteArray rawEmail =
            KPIMUtils::extractEmailAddress( contact.preferredEmail().toUtf8() );
          if ( !rawEmail.isEmpty() ) {
            newRights[ rawEmail ] = it.value();
          }
        }
      }
    } else { // it has been a normal contact
      const QByteArray rawEmail = KPIMUtils::extractEmailAddress( it.key() );
      if ( !rawEmail.isEmpty() ) {
        newRights[ rawEmail ] = it.value();
      }
    }
  }

  attribute->setRights( newRights );

  new Akonadi::CollectionModifyJob( d->mCollection );
}

#include "aclmanager.moc"
