/*
    Copyright (c) 2009 Tobias Koenig <tokoe@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 "openxchangeresource.h"

#include "configdialog.h"
#include "settingsadaptor.h"

#include <cachepolicy.h>
#include <changerecorder.h>
#include <collectionfetchjob.h>
#include <collectionfetchscope.h>
#include <entitydisplayattribute.h>
#include <itemfetchscope.h>
#include <AkonadiCore/vectorhelper.h>

#include <kcontacts/addressee.h>
#include <KCalCore/Event>
#include <KCalCore/Todo>
#include <KLocalizedString>

#include <oxa/davmanager.h>
#include <oxa/oxerrors.h>
#include <oxa/foldercreatejob.h>
#include <oxa/folderdeletejob.h>
#include <oxa/foldermodifyjob.h>
#include <oxa/foldermovejob.h>
#include <oxa/foldersrequestdeltajob.h>
#include <oxa/foldersrequestjob.h>
#include <oxa/objectcreatejob.h>
#include <oxa/objectdeletejob.h>
#include <oxa/objectmodifyjob.h>
#include <oxa/objectmovejob.h>
#include <oxa/objectrequestjob.h>
#include <oxa/objectsrequestdeltajob.h>
#include <oxa/objectsrequestjob.h>
#include <oxa/updateusersjob.h>
#include <oxa/users.h>
#include <QStandardPaths>
#include <QIcon>

using namespace Akonadi;

class RemoteInformation
{
public:
    RemoteInformation(qlonglong objectId, OXA::Folder::Module module, const QString &lastModified)
        : mObjectId(objectId),
          mModule(module),
          mLastModified(lastModified)
    {
    }

    inline qlonglong objectId() const
    {
        return mObjectId;
    }

    inline OXA::Folder::Module module() const
    {
        return mModule;
    }

    inline QString lastModified() const
    {
        return mLastModified;
    }

    inline void setLastModified(const QString &lastModified)
    {
        mLastModified = lastModified;
    }

    static RemoteInformation load(const Item &item)
    {
        return loadImpl(item);
    }

    static RemoteInformation load(const Collection &collection)
    {
        return loadImpl(collection);
    }

    void store(Item &item) const
    {
        storeImpl(item);
    }

    void store(Collection &collection) const
    {
        storeImpl(collection);
    }

private:
    template<typename T>
    static inline RemoteInformation loadImpl(const T &entity)
    {
        const QStringList parts = entity.remoteRevision().split(QLatin1Char(':'), QString::KeepEmptyParts);

        OXA::Folder::Module module = OXA::Folder::Unbound;

        if (parts.count() > 0) {
            if (parts.at(0) == QLatin1String("calendar")) {
                module = OXA::Folder::Calendar;
            } else if (parts.at(0) == QLatin1String("contacts")) {
                module = OXA::Folder::Contacts;
            } else if (parts.at(0) == QLatin1String("tasks")) {
                module = OXA::Folder::Tasks;
            } else {
                module = OXA::Folder::Unbound;
            }
        }

        QString lastModified = QStringLiteral("0");
        if (parts.count() > 1) {
            lastModified = parts.at(1);
        }

        return RemoteInformation(entity.remoteId().toLongLong(), module, lastModified);
    }

    template<typename T>
    inline void storeImpl(T &entity) const
    {
        QString module;
        switch (mModule) {
        case OXA::Folder::Calendar:
            module = QStringLiteral("calendar");
            break;
        case OXA::Folder::Contacts:
            module = QStringLiteral("contacts");
            break;
        case OXA::Folder::Tasks:
            module = QStringLiteral("tasks");
            break;
        case OXA::Folder::Unbound:
            break;
        }

        QStringList parts;
        parts.append(module);
        parts.append(mLastModified);

        entity.setRemoteId(QString::number(mObjectId));
        entity.setRemoteRevision(parts.join(QStringLiteral(":")));
    }

    qlonglong mObjectId;
    OXA::Folder::Module mModule;
    QString mLastModified;
};

class ObjectsLastSync
{
public:
    ObjectsLastSync()
    {
        if (!Settings::self()->objectsLastSync().isEmpty()) {
            const QStringList pairs = Settings::self()->objectsLastSync().split(QLatin1Char(':'), QString::KeepEmptyParts);
            foreach (const QString &pair, pairs) {
                const QStringList entry = pair.split(QLatin1Char('='), QString::KeepEmptyParts);
                mObjectsMap.insert(entry.at(0).toLongLong(), entry.at(1).toULongLong());
            }
        }
    }

    void save()
    {
        QStringList pairs;
        pairs.reserve(mObjectsMap.count());

        QMapIterator<qlonglong, qulonglong> it(mObjectsMap);
        while (it.hasNext()) {
            it.next();
            pairs.append(QString::number(it.key()) + QLatin1Char('=') + QString::number(it.value()));
        }

        Settings::self()->setObjectsLastSync(pairs.join(QStringLiteral(":")));
        Settings::self()->save();
    }

    qulonglong lastSync(qlonglong collectionId) const
    {
        if (!mObjectsMap.contains(collectionId)) {
            return 0;
        }

        return mObjectsMap.value(collectionId);
    }

    void setLastSync(qlonglong collectionId, qulonglong timeStamp)
    {
        mObjectsMap.insert(collectionId, timeStamp);
    }

private:
    QMap<qlonglong, qulonglong> mObjectsMap;
};

static Collection::Rights folderPermissionsToCollectionRights(const OXA::Folder &folder)
{
    const OXA::Folder::UserPermissions userPermissions = folder.userPermissions();

    if (!userPermissions.contains(OXA::Users::self()->currentUserId())) {
        // There are no rights given for us explicitly, so it is read-only
        return Collection::ReadOnly;
    } else {
        const OXA::Folder::Permissions permissions = userPermissions.value(OXA::Users::self()->currentUserId());
        Collection::Rights rights = Collection::ReadOnly;
        switch (permissions.folderPermission()) {
        case OXA::Folder::Permissions::FolderIsVisible: rights |= Collection::ReadOnly; break;
        case OXA::Folder::Permissions::CreateObjects: rights |= Collection::CanCreateItem; break;
        case OXA::Folder::Permissions::CreateSubfolders: // fallthrough
        case OXA::Folder::Permissions::AdminPermission: rights |= (Collection::CanCreateItem | Collection::CanCreateCollection); break;
        default: break;
        }

        if (permissions.objectWritePermission() != OXA::Folder::Permissions::NoWritePermission) {
            rights |= Collection::CanChangeItem;
            rights |= Collection::CanChangeCollection;
        }

        if (permissions.objectDeletePermission() != OXA::Folder::Permissions::NoDeletePermission) {
            rights |= Collection::CanDeleteItem;
            rights |= Collection::CanDeleteCollection;
        }

        return rights;
    }
}

OpenXchangeResource::OpenXchangeResource(const QString &id)
    : ResourceBase(id)
{
    // setup the resource
    new SettingsAdaptor(Settings::self());
    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"),
            Settings::self(), QDBusConnection::ExportAdaptors);

    changeRecorder()->fetchCollection(true);
    changeRecorder()->itemFetchScope().fetchFullPayload(true);
    changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::Parent);
    changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::Parent);

    setName(i18n("Open-Xchange"));

    OXA::Users::self()->init(identifier());

    QUrl baseUrl = QUrl::fromLocalFile(Settings::self()->baseUrl());
    baseUrl.setUserName(Settings::self()->username());
    baseUrl.setPassword(Settings::self()->password());
    OXA::DavManager::self()->setBaseUrl(baseUrl);

    // Create the standard collections.
    //
    // There exists special OX folders (e.g. private, public, shared) that are not
    // returned by a normal webdav listing, therefor we create them manually here.
    // This is possible because the remote ids of these folders are fixed values from 1
    // till 4.
    mResourceCollection.setParentCollection(Collection::root());
    const RemoteInformation resourceInformation(0, OXA::Folder::Unbound, QString());
    resourceInformation.store(mResourceCollection);
    mResourceCollection.setName(name());
    mResourceCollection.setContentMimeTypes(QStringList() << Collection::mimeType());
    mResourceCollection.setRights(Collection::ReadOnly);
    EntityDisplayAttribute *attribute = mResourceCollection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
    attribute->setIconName(QStringLiteral("ox"));

    Collection privateFolder;
    privateFolder.setParentCollection(mResourceCollection);
    const RemoteInformation privateFolderInformation(1, OXA::Folder::Unbound, QString());
    privateFolderInformation.store(privateFolder);
    privateFolder.setName(i18n("Private Folder"));
    privateFolder.setContentMimeTypes(QStringList() << Collection::mimeType());
    privateFolder.setRights(Collection::ReadOnly);

    Collection publicFolder;
    publicFolder.setParentCollection(mResourceCollection);
    const RemoteInformation publicFolderInformation(2, OXA::Folder::Unbound, QString());
    publicFolderInformation.store(publicFolder);
    publicFolder.setName(i18n("Public Folder"));
    publicFolder.setContentMimeTypes(QStringList() << Collection::mimeType());
    publicFolder.setRights(Collection::ReadOnly);

    Collection sharedFolder;
    sharedFolder.setParentCollection(mResourceCollection);
    const RemoteInformation sharedFolderInformation(3, OXA::Folder::Unbound, QString());
    sharedFolderInformation.store(sharedFolder);
    sharedFolder.setName(i18n("Shared Folder"));
    sharedFolder.setContentMimeTypes(QStringList() << Collection::mimeType());
    sharedFolder.setRights(Collection::ReadOnly);

    Collection systemFolder;
    systemFolder.setParentCollection(mResourceCollection);
    const RemoteInformation systemFolderInformation(4, OXA::Folder::Unbound, QString());
    systemFolderInformation.store(systemFolder);
    systemFolder.setName(i18n("System Folder"));
    systemFolder.setContentMimeTypes(QStringList() << Collection::mimeType());
    systemFolder.setRights(Collection::ReadOnly);

    // TODO: set cache policy depending on sync behaviour
    Akonadi::CachePolicy cachePolicy;
    cachePolicy.setInheritFromParent(false);
    cachePolicy.setSyncOnDemand(false);
    cachePolicy.setCacheTimeout(-1);
    cachePolicy.setIntervalCheckTime(5);
    mResourceCollection.setCachePolicy(cachePolicy);

    mStandardCollectionsMap.insert(0, mResourceCollection);
    mStandardCollectionsMap.insert(1, privateFolder);
    mStandardCollectionsMap.insert(2, publicFolder);
    mStandardCollectionsMap.insert(3, sharedFolder);
    mStandardCollectionsMap.insert(4, systemFolder);

    mCollectionsMap = mStandardCollectionsMap;

    if (Settings::self()->useIncrementalUpdates()) {
        syncCollectionsRemoteIdCache();
    }
}

OpenXchangeResource::~OpenXchangeResource()
{
}

void OpenXchangeResource::cleanup()
{
    // be nice and remove cache file when resource is removed
    QFile::remove(OXA::Users::self()->cacheFilePath());

    QFile::remove(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + Settings::self()->config()->name()) ;

    ResourceBase::cleanup();
}

void OpenXchangeResource::aboutToQuit()
{
}

void OpenXchangeResource::configure(WId windowId)
{
    const bool useIncrementalUpdates = Settings::self()->useIncrementalUpdates();

    ConfigDialog dlg(windowId);
    dlg.setWindowIcon(QIcon::fromTheme(QStringLiteral("ox")));
    if (dlg.exec()) {   //krazy:exclude=crashy

        // if the user has changed the incremental update settings we have to do
        // some additional initialization work
        if (useIncrementalUpdates != Settings::self()->useIncrementalUpdates()) {
            Settings::self()->setFoldersLastSync(0);
            Settings::self()->setObjectsLastSync(QString());
        }

        Settings::self()->save();

        clearCache();

        QUrl baseUrl = QUrl::fromLocalFile(Settings::self()->baseUrl());
        baseUrl.setUserName(Settings::self()->username());
        baseUrl.setPassword(Settings::self()->password());
        OXA::DavManager::self()->setBaseUrl(baseUrl);

        // To find out the correct ACLs we need the uid of the user that
        // logs in. For loading events and tasks we need a complete mapping of
        // user id to name as well, so the mapping must be loaded as well.
        // Both is done by UpdateUsersJob, so trigger it here before we continue
        // with synchronization in onUpdateUsersJobFinished.
        OXA::UpdateUsersJob *job = new OXA::UpdateUsersJob(this);
        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onUpdateUsersJobFinished);
        job->start();
    } else {
        Q_EMIT configurationDialogRejected();
    }
}

void OpenXchangeResource::retrieveCollections()
{
    //qDebug("tokoe: retrieve collections called");
    if (Settings::self()->useIncrementalUpdates()) {
        //qDebug( "lastSync=%llu", Settings::self()->foldersLastSync() );
        OXA::FoldersRequestDeltaJob *job = new OXA::FoldersRequestDeltaJob(Settings::self()->foldersLastSync(), this);
        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFoldersRequestDeltaJobFinished);
        job->start();
    } else {
        OXA::FoldersRequestJob *job = new OXA::FoldersRequestJob(0, OXA::FoldersRequestJob::Modified, this);
        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFoldersRequestJobFinished);
        job->start();
    }
}

void OpenXchangeResource::retrieveItems(const Akonadi::Collection &collection)
{
    //qDebug( "tokoe: retrieveItems on %s called", qPrintable( collection.name() ) );
    const RemoteInformation remoteInformation = RemoteInformation::load(collection);

    OXA::Folder folder;
    folder.setObjectId(remoteInformation.objectId());
    folder.setModule(remoteInformation.module());

    if (Settings::self()->useIncrementalUpdates()) {
        ObjectsLastSync lastSyncInfo;
        //qDebug( "lastSync=%llu", lastSyncInfo.lastSync( collection.id() ) );
        OXA::ObjectsRequestDeltaJob *job = new OXA::ObjectsRequestDeltaJob(folder, lastSyncInfo.lastSync(collection.id()), this);
        job->setProperty("collection", QVariant::fromValue(collection));
        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectsRequestDeltaJobFinished);
        job->start();
    } else {
        OXA::ObjectsRequestJob *job = new OXA::ObjectsRequestJob(folder, 0, OXA::ObjectsRequestJob::Modified, this);
        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectsRequestJobFinished);
        job->start();
    }
}

bool OpenXchangeResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
{
    //qDebug( "tokoe: retrieveItem %lld called", item.id() );
    const RemoteInformation remoteInformation = RemoteInformation::load(item);

    OXA::Object object;
    object.setObjectId(remoteInformation.objectId());
    object.setModule(remoteInformation.module());

    OXA::ObjectRequestJob *job = new OXA::ObjectRequestJob(object, this);
    job->setProperty("item", QVariant::fromValue(item));
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectRequestJobFinished);
    job->start();

    return true;
}

void OpenXchangeResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
{
    const RemoteInformation remoteInformation = RemoteInformation::load(collection);

    OXA::Object object;
    object.setFolderId(remoteInformation.objectId());
    object.setModule(remoteInformation.module());

    if (item.hasPayload<KContacts::Addressee>()) {
        object.setContact(item.payload<KContacts::Addressee>());
    } else if (item.hasPayload<KContacts::ContactGroup>()) {
        object.setContactGroup(item.payload<KContacts::ContactGroup>());
    } else if (item.hasPayload<KCalCore::Event::Ptr>()) {
        object.setEvent(item.payload<KCalCore::Incidence::Ptr>());
    } else if (item.hasPayload<KCalCore::Todo::Ptr>()) {
        object.setTask(item.payload<KCalCore::Incidence::Ptr>());
    } else {
        Q_ASSERT(false);
    }

    OXA::ObjectCreateJob *job = new OXA::ObjectCreateJob(object, this);
    job->setProperty("item", QVariant::fromValue(item));
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectCreateJobFinished);
    job->start();
}

void OpenXchangeResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
{
    const RemoteInformation remoteInformation = RemoteInformation::load(item);
    const RemoteInformation parentRemoteInformation = RemoteInformation::load(item.parentCollection());

    OXA::Object object;
    object.setObjectId(remoteInformation.objectId());
    object.setModule(remoteInformation.module());
    object.setFolderId(parentRemoteInformation.objectId());
    object.setLastModified(remoteInformation.lastModified());

    if (item.hasPayload<KContacts::Addressee>()) {
        object.setContact(item.payload<KContacts::Addressee>());
    } else if (item.hasPayload<KContacts::ContactGroup>()) {
        object.setContactGroup(item.payload<KContacts::ContactGroup>());
    } else if (item.hasPayload<KCalCore::Event::Ptr>()) {
        object.setEvent(item.payload<KCalCore::Incidence::Ptr>());
    } else if (item.hasPayload<KCalCore::Todo::Ptr>()) {
        object.setTask(item.payload<KCalCore::Incidence::Ptr>());
    } else {
        Q_ASSERT(false);
    }

    OXA::ObjectModifyJob *job = new OXA::ObjectModifyJob(object, this);
    job->setProperty("item", QVariant::fromValue(item));
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectModifyJobFinished);
    job->start();
}

void OpenXchangeResource::itemRemoved(const Akonadi::Item &item)
{
    const RemoteInformation remoteInformation = RemoteInformation::load(item);
    const RemoteInformation parentRemoteInformation = RemoteInformation::load(item.parentCollection());

    OXA::Object object;
    object.setObjectId(remoteInformation.objectId());
    object.setFolderId(parentRemoteInformation.objectId());
    object.setModule(remoteInformation.module());
    object.setLastModified(remoteInformation.lastModified());

    OXA::ObjectDeleteJob *job = new OXA::ObjectDeleteJob(object, this);
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectDeleteJobFinished);

    job->start();
}

void OpenXchangeResource::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource,
                                    const Akonadi::Collection &collectionDestination)
{
    const RemoteInformation remoteInformation = RemoteInformation::load(item);
    const RemoteInformation parentRemoteInformation = RemoteInformation::load(collectionSource);
    const RemoteInformation newParentRemoteInformation = RemoteInformation::load(collectionDestination);

    OXA::Object object;
    object.setObjectId(remoteInformation.objectId());
    object.setModule(remoteInformation.module());
    object.setFolderId(parentRemoteInformation.objectId());
    object.setLastModified(remoteInformation.lastModified());

    OXA::Folder destinationFolder;
    destinationFolder.setObjectId(newParentRemoteInformation.objectId());

    OXA::ObjectMoveJob *job = new OXA::ObjectMoveJob(object, destinationFolder, this);
    job->setProperty("item", QVariant::fromValue(item));
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectMoveJobFinished);

    job->start();
}

void OpenXchangeResource::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
{
    const RemoteInformation parentRemoteInformation = RemoteInformation::load(parent);

    OXA::Folder folder;
    folder.setTitle(collection.name());
    folder.setFolderId(parentRemoteInformation.objectId());
    folder.setType(OXA::Folder::Private);

    // the folder 'inherits' the module type of its parent collection
    folder.setModule(parentRemoteInformation.module());

    // fill permissions
    OXA::Folder::Permissions permissions;
    permissions.setFolderPermission(OXA::Folder::Permissions::CreateSubfolders);
    permissions.setObjectReadPermission(OXA::Folder::Permissions::ReadOwnObjects);
    permissions.setObjectWritePermission(OXA::Folder::Permissions::WriteOwnObjects);
    permissions.setObjectDeletePermission(OXA::Folder::Permissions::DeleteOwnObjects);
    permissions.setAdminFlag(true);

    // assign permissions to user
    OXA::Folder::UserPermissions userPermissions;
    userPermissions.insert(OXA::Users::self()->currentUserId(), permissions);

    // set user permissions of folder
    folder.setUserPermissions(userPermissions);

    OXA::FolderCreateJob *job = new OXA::FolderCreateJob(folder, this);
    job->setProperty("collection", QVariant::fromValue(collection));
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFolderCreateJobFinished);
    job->start();
}

void OpenXchangeResource::collectionChanged(const Akonadi::Collection &collection)
{
    const RemoteInformation remoteInformation = RemoteInformation::load(collection);

    // do not try to change the standard collections
    if (remoteInformation.objectId() >= 0 && remoteInformation.objectId() <= 4) {
        changeCommitted(collection);
        return;
    }

    const RemoteInformation parentRemoteInformation = RemoteInformation::load(collection.parentCollection());

    OXA::Folder folder;
    folder.setObjectId(remoteInformation.objectId());
    folder.setFolderId(parentRemoteInformation.objectId());
    folder.setTitle(collection.name());
    folder.setLastModified(remoteInformation.lastModified());

    OXA::FolderModifyJob *job = new OXA::FolderModifyJob(folder, this);
    job->setProperty("collection", QVariant::fromValue(collection));
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFolderModifyJobFinished);
}

void OpenXchangeResource::collectionRemoved(const Akonadi::Collection &collection)
{
    const RemoteInformation remoteInformation = RemoteInformation::load(collection);

    OXA::Folder folder;
    folder.setObjectId(remoteInformation.objectId());
    folder.setLastModified(remoteInformation.lastModified());

    OXA::FolderDeleteJob *job = new OXA::FolderDeleteJob(folder, this);
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFolderDeleteJobFinished);

    job->start();
}

void OpenXchangeResource::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource,
        const Akonadi::Collection &collectionDestination)
{
    const RemoteInformation remoteInformation = RemoteInformation::load(collection);
    const RemoteInformation parentRemoteInformation = RemoteInformation::load(collectionSource);
    const RemoteInformation newParentRemoteInformation = RemoteInformation::load(collectionDestination);

    OXA::Folder folder;
    folder.setObjectId(remoteInformation.objectId());
    folder.setFolderId(parentRemoteInformation.objectId());

    OXA::Folder destinationFolder;
    destinationFolder.setObjectId(newParentRemoteInformation.objectId());

    OXA::FolderMoveJob *job = new OXA::FolderMoveJob(folder, destinationFolder, this);
    job->setProperty("collection", QVariant::fromValue(collection));
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFolderMoveJobFinished);

    job->start();
}

//// job result slots

void OpenXchangeResource::onUpdateUsersJobFinished(KJob *job)
{
    if (job->error()) {
        // This might be an indication that we can not connect to the server...
        Q_EMIT status(Broken, i18n("Unable to connect to server"));
        return;
    }

    if (Settings::self()->useIncrementalUpdates()) {
        syncCollectionsRemoteIdCache();
    }

    // now we have all user information, so continue synchronization
    synchronize();
    Q_EMIT configurationDialogAccepted();
}

void OpenXchangeResource::onObjectsRequestJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::ObjectsRequestJob *requestJob = qobject_cast<OXA::ObjectsRequestJob *>(job);
    Q_ASSERT(requestJob);

    Item::List items;

    const OXA::Object::List objects = requestJob->objects();
    foreach (const OXA::Object &object, objects) {
        Item item;
        switch (object.module()) {
        case OXA::Folder::Contacts:
            if (!object.contact().isEmpty()) {
                item.setMimeType(KContacts::Addressee::mimeType());
                item.setPayload<KContacts::Addressee>(object.contact());
            } else {
                item.setMimeType(KContacts::ContactGroup::mimeType());
                item.setPayload<KContacts::ContactGroup>(object.contactGroup());
            }
            break;
        case OXA::Folder::Calendar:
            item.setMimeType(KCalCore::Event::eventMimeType());
            item.setPayload<KCalCore::Incidence::Ptr>(object.event());
            break;
        case OXA::Folder::Tasks:
            item.setMimeType(KCalCore::Todo::todoMimeType());
            item.setPayload<KCalCore::Incidence::Ptr>(object.task());
            break;
        case OXA::Folder::Unbound:
            Q_ASSERT(false);
            break;
        }
        const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
        remoteInformation.store(item);

        items.append(item);
    }

    itemsRetrieved(items);
}

void OpenXchangeResource::onObjectsRequestDeltaJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::ObjectsRequestDeltaJob *requestJob = qobject_cast<OXA::ObjectsRequestDeltaJob *>(job);
    Q_ASSERT(requestJob);

    const Akonadi::Collection collection = requestJob->property("collection").value<Collection>();

    ObjectsLastSync lastSyncInfo;

    qulonglong objectsLastSync = lastSyncInfo.lastSync(collection.id());

    Item::List changedItems;

    const OXA::Object::List modifiedObjects = requestJob->modifiedObjects();
    foreach (const OXA::Object &object, modifiedObjects) {
        Item item;
        switch (object.module()) {
        case OXA::Folder::Contacts:
            if (!object.contact().isEmpty()) {
                item.setMimeType(KContacts::Addressee::mimeType());
                item.setPayload<KContacts::Addressee>(object.contact());
            } else {
                item.setMimeType(KContacts::ContactGroup::mimeType());
                item.setPayload<KContacts::ContactGroup>(object.contactGroup());
            }
            break;
        case OXA::Folder::Calendar:
            item.setMimeType(KCalCore::Event::eventMimeType());
            item.setPayload<KCalCore::Incidence::Ptr>(object.event());
            break;
        case OXA::Folder::Tasks:
            item.setMimeType(KCalCore::Todo::todoMimeType());
            item.setPayload<KCalCore::Incidence::Ptr>(object.task());
            break;
        case OXA::Folder::Unbound:
            Q_ASSERT(false);
            break;
        }
        const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
        remoteInformation.store(item);

        // the value of objectsLastSync is determined by the maximum last modified value
        // of the added or changed objects
        objectsLastSync = qMax(objectsLastSync, object.lastModified().toULongLong());

        changedItems.append(item);
    }

    Item::List removedItems;

    const OXA::Object::List deletedObjects = requestJob->deletedObjects();
    removedItems.reserve(deletedObjects.count());
    foreach (const OXA::Object &object, deletedObjects) {
        Item item;

        const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
        remoteInformation.store(item);

        removedItems.append(item);
    }

    if (objectsLastSync != lastSyncInfo.lastSync(collection.id())) {
        // according to the OX developers we should subtract one millisecond from the
        // maximum last modified value to cover multiple changes that might have been
        // done in the same millisecond to the data on the server
        lastSyncInfo.setLastSync(collection.id(), objectsLastSync - 1);
        lastSyncInfo.save();
    }

    //qDebug( "changedObjects=%d removedObjects=%d", modifiedObjects.count(), deletedObjects.count() );
    //qDebug( "changedItems=%d removedItems=%d", changedItems.count(), removedItems.count() );
    itemsRetrievedIncremental(changedItems, removedItems);
}

void OpenXchangeResource::onObjectRequestJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::ObjectRequestJob *requestJob = qobject_cast<OXA::ObjectRequestJob *>(job);
    Q_ASSERT(requestJob);

    const OXA::Object object = requestJob->object();

    Item item = job->property("item").value<Item>();

    const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
    remoteInformation.store(item);

    switch (object.module()) {
    case OXA::Folder::Contacts:
        if (!object.contact().isEmpty()) {
            item.setMimeType(KContacts::Addressee::mimeType());
            item.setPayload<KContacts::Addressee>(object.contact());
        } else {
            item.setMimeType(KContacts::ContactGroup::mimeType());
            item.setPayload<KContacts::ContactGroup>(object.contactGroup());
        }
        break;
    case OXA::Folder::Calendar:
        item.setMimeType(KCalCore::Event::eventMimeType());
        item.setPayload<KCalCore::Incidence::Ptr>(object.event());
        break;
    case OXA::Folder::Tasks:
        item.setMimeType(KCalCore::Todo::todoMimeType());
        item.setPayload<KCalCore::Incidence::Ptr>(object.task());
        break;
    case OXA::Folder::Unbound:
        Q_ASSERT(false);
        break;
    }

    itemRetrieved(item);
}

void OpenXchangeResource::onObjectCreateJobFinished(KJob *job)
{
    if (job->error()) {
        QString errorText = job->errorText();
        if (job->error() == KJob::UserDefinedError) {
            switch (OXA::OXErrors::getEditErrorID(job->errorText())) {
            case OXA::OXErrors::ConcurrentModification : errorText = i18n("The object was edited by another participant in the meantime. Please check."); break;
            case OXA::OXErrors::ObjectNotFound : errorText = i18n("Object not found. Maybe it was deleted by another participant in the meantime."); break;
            case OXA::OXErrors::NoPermissionForThisAction : errorText = i18n("You don't have the permission to perform this action on this object."); break;
            case OXA::OXErrors::ConflictsDetected : errorText = i18n("A conflict detected. Please check if there are other objects in conflict with this one."); break;
            case OXA::OXErrors::MissingMandatoryFields : errorText = i18n("A mandatory data field is missing. Please check. Otherwise contact your administrator."); break;
            case OXA::OXErrors::AppointmentConflicts : errorText = i18n("An appointment conflict detected.\nPlease check if there are other appointments in conflict with this one."); break;
            case OXA::OXErrors::InternalServerError : errorText = i18n("Internal server error. Please contact your administrator."); break;
            case OXA::OXErrors::EditErrorUndefined :
            default : ;
            }
        }
        cancelTask(errorText);
        return;
    }

    OXA::ObjectCreateJob *createJob = qobject_cast<OXA::ObjectCreateJob *>(job);
    Q_ASSERT(createJob);

    const OXA::Object object = createJob->object();

    Item item = job->property("item").value<Item>();

    const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
    remoteInformation.store(item);

    changeCommitted(item);
}

void OpenXchangeResource::onObjectModifyJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::ObjectModifyJob *modifyJob = qobject_cast<OXA::ObjectModifyJob *>(job);
    Q_ASSERT(modifyJob);

    const OXA::Object object = modifyJob->object();

    Item item = job->property("item").value<Item>();

    // update last_modified property
    RemoteInformation remoteInformation = RemoteInformation::load(item);
    remoteInformation.setLastModified(object.lastModified());
    remoteInformation.store(item);

    changeCommitted(item);
}

void OpenXchangeResource::onObjectMoveJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::ObjectMoveJob *moveJob = qobject_cast<OXA::ObjectMoveJob *>(job);
    Q_ASSERT(moveJob);

    const OXA::Object object = moveJob->object();

    Item item = job->property("item").value<Item>();

    // update last_modified property
    RemoteInformation remoteInformation = RemoteInformation::load(item);
    remoteInformation.setLastModified(object.lastModified());
    remoteInformation.store(item);

    changeCommitted(item);
}

void OpenXchangeResource::onObjectDeleteJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    changeProcessed();
}

static Collection folderToCollection(const OXA::Folder &folder, const Collection &parentCollection)
{
    Collection collection;

    collection.setParentCollection(parentCollection);

    const RemoteInformation remoteInformation(folder.objectId(), folder.module(), folder.lastModified());
    remoteInformation.store(collection);

    // set a unique name to make Akonadi happy
    collection.setName(folder.title() + QLatin1Char('_') + QUuid::createUuid().toString());

    EntityDisplayAttribute *attribute = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
    attribute->setDisplayName(folder.title());

    QStringList mimeTypes;
    mimeTypes.append(Collection::mimeType());
    switch (folder.module()) {
    case OXA::Folder::Calendar:
        mimeTypes.append(KCalCore::Event::eventMimeType());
        attribute->setIconName(QStringLiteral("view-calendar"));
        break;
    case OXA::Folder::Contacts:
        mimeTypes.append(KContacts::Addressee::mimeType());
        mimeTypes.append(KContacts::ContactGroup::mimeType());
        attribute->setIconName(QStringLiteral("view-pim-contacts"));
        break;
    case OXA::Folder::Tasks:
        mimeTypes.append(KCalCore::Todo::todoMimeType());
        attribute->setIconName(QStringLiteral("view-pim-tasks"));
        break;
    case OXA::Folder::Unbound:
        break;
    }

    collection.setContentMimeTypes(mimeTypes);
    collection.setRights(folderPermissionsToCollectionRights(folder));

    return collection;
}

void OpenXchangeResource::onFoldersRequestJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::FoldersRequestJob *requestJob = qobject_cast<OXA::FoldersRequestJob *>(job);
    Q_ASSERT(requestJob);

    Collection::List collections;

    // add the standard collections
    collections << Akonadi::valuesToVector(mStandardCollectionsMap);

    QMap<qlonglong, Collection> remoteIdMap(mStandardCollectionsMap);

    // add the folders from the server
    OXA::Folder::List folders = requestJob->folders();
    while (!folders.isEmpty()) {
        const OXA::Folder folder = folders.takeFirst();
        if (remoteIdMap.contains(folder.folderId())) {
            // we have the parent collection created already
            const Collection collection = folderToCollection(folder, remoteIdMap.value(folder.folderId()));
            remoteIdMap.insert(folder.objectId(), collection);
            collections.append(collection);
        } else {
            // we have to wait until the parent folder has been created
            folders.append(folder);
            qDebug() << "Error: parent folder id" << folder.folderId() << "of folder" << folder.title() << "is unknown";
        }
    }

    collectionsRetrieved(collections);
}

void OpenXchangeResource::onFoldersRequestDeltaJobFinished(KJob *job)
{
    //qDebug( "onFoldersRequestDeltaJobFinished mCollectionsMap.count() = %d", mCollectionsMap.count() );

    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::FoldersRequestDeltaJob *requestJob = qobject_cast<OXA::FoldersRequestDeltaJob *>(job);
    Q_ASSERT(requestJob);

    Collection::List changedCollections;

    // add the standard collections
    changedCollections << Akonadi::valuesToVector(mStandardCollectionsMap);

    qulonglong foldersLastSync = Settings::self()->foldersLastSync();

    // add the new or modified folders from the server
    OXA::Folder::List modifiedFolders = requestJob->modifiedFolders();
    while (!modifiedFolders.isEmpty()) {
        const OXA::Folder folder = modifiedFolders.takeFirst();
        if (mCollectionsMap.contains(folder.folderId())) {
            // we have the parent collection created already
            const Collection collection = folderToCollection(folder, mCollectionsMap.value(folder.folderId()));
            mCollectionsMap.insert(folder.objectId(), collection);
            changedCollections.append(collection);

            // the value of foldersLastSync is determined by the maximum last modified value
            // of the added or changed folders
            foldersLastSync = qMax(foldersLastSync, folder.lastModified().toULongLong());

        } else {
            // we have to wait until the parent folder has been created
            modifiedFolders.append(folder);
            qDebug() << "Error: parent folder id" << folder.folderId() << "of folder" << folder.title() << "is unknown";
        }
    }

    Collection::List removedCollections;

    // add the deleted folders from the server
    OXA::Folder::List deletedFolders = requestJob->deletedFolders();
    removedCollections.reserve(deletedFolders.count());
    foreach (const OXA::Folder &folder, deletedFolders) {
        Collection collection;
        collection.setRemoteId(QString::number(folder.objectId()));

        removedCollections.append(collection);
    }

    if (foldersLastSync != Settings::self()->foldersLastSync()) {
        // according to the OX developers we should subtract one millisecond from the
        // maximum last modified value to cover multiple changes that might have been
        // done in the same millisecond to the data on the server
        Settings::self()->setFoldersLastSync(foldersLastSync - 1);
        Settings::self()->save();
    }

    //qDebug( "changedFolders=%d removedFolders=%d", modifiedFolders.count(), deletedFolders.count() );
    //qDebug( "changedCollections=%d removedCollections=%d", changedCollections.count(), removedCollections.count() );
    collectionsRetrievedIncremental(changedCollections, removedCollections);
}

void OpenXchangeResource::onFolderCreateJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::FolderCreateJob *createJob = qobject_cast<OXA::FolderCreateJob *>(job);
    Q_ASSERT(createJob);

    const OXA::Folder folder = createJob->folder();

    Collection collection = job->property("collection").value<Collection>();

    const RemoteInformation remoteInformation(folder.objectId(), folder.module(), folder.lastModified());
    remoteInformation.store(collection);

    // set matching icon
    EntityDisplayAttribute *attribute = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
    switch (folder.module()) {
    case OXA::Folder::Calendar:
        attribute->setIconName(QStringLiteral("view-calendar"));
        break;
    case OXA::Folder::Contacts:
        attribute->setIconName(QStringLiteral("view-pim-contacts"));
        break;
    case OXA::Folder::Tasks:
        attribute->setIconName(QStringLiteral("view-pim-tasks"));
        break;
    case OXA::Folder::Unbound:
        break;
    }

    changeCommitted(collection);
}

void OpenXchangeResource::onFolderModifyJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::FolderModifyJob *modifyJob = qobject_cast<OXA::FolderModifyJob *>(job);
    Q_ASSERT(modifyJob);

    const OXA::Folder folder = modifyJob->folder();

    Collection collection = job->property("collection").value<Collection>();

    // update last_modified property
    RemoteInformation remoteInformation = RemoteInformation::load(collection);
    remoteInformation.setLastModified(folder.lastModified());
    remoteInformation.store(collection);

    changeCommitted(collection);
}

void OpenXchangeResource::onFolderMoveJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    OXA::FolderMoveJob *moveJob = qobject_cast<OXA::FolderMoveJob *>(job);
    Q_ASSERT(moveJob);

    const OXA::Folder folder = moveJob->folder();

    Collection collection = job->property("collection").value<Collection>();

    // update last_modified property
    RemoteInformation remoteInformation = RemoteInformation::load(collection);
    remoteInformation.setLastModified(folder.lastModified());
    remoteInformation.store(collection);

    changeCommitted(collection);
}

void OpenXchangeResource::onFolderDeleteJobFinished(KJob *job)
{
    if (job->error()) {
        cancelTask(job->errorText());
        return;
    }

    changeProcessed();
}

/**
 * For incremental updates we need a mapping between the folder id
 * and the collection object for all collections of this resource,
 * so that we can find out the right parent collection in
 * onFoldersRequestDeltaJob().
 *
 * Therefor we trigger this method when the resource is started and
 * configured to use incremental sync.
 */
void OpenXchangeResource::syncCollectionsRemoteIdCache()
{
    mCollectionsMap.clear();

    // copy the standard collections
    mCollectionsMap = mStandardCollectionsMap;

    CollectionFetchJob *job = new CollectionFetchJob(mResourceCollection, CollectionFetchJob::Recursive, this);
    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFetchResourceCollectionsFinished);
}

void OpenXchangeResource::onFetchResourceCollectionsFinished(KJob *job)
{
    if (job->error()) {
        qDebug() << "Error: Unable to fetch resource collections:" << job->errorText();
        return;
    }

    const CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>(job);

    // copy the remaining collections of the resource
    const Collection::List collections = fetchJob->collections();
    foreach (const Collection &collection, collections) {
        mCollectionsMap.insert(collection.remoteId().toULongLong(), collection);
    }
}

AKONADI_RESOURCE_MAIN(OpenXchangeResource)

