// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2013 Utku Aydın <utkuaydin34@gmail.com>
//

#include "BookmarkSyncManager.h"

#include "BookmarkManager.h"
#include "CloudSyncManager.h"
#include "GeoDataCoordinates.h"
#include "GeoDataDocument.h"
#include "GeoDataFolder.h"
#include "GeoDataLookAt.h"
#include "GeoDataParser.h"
#include "GeoWriter.h"
#include "MarbleDebug.h"
#include "MarbleDirs.h"
#include "MarbleModel.h"
#include "OwncloudSyncBackend.h"

#include <QBuffer>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QTimer>

namespace Marble
{

class DiffItem
{
public:
    enum Action {
        NoAction,
        Created,
        Changed,
        Deleted
    };

    enum Status {
        Source,
        Destination
    };

    QString m_path;
    Action m_action;
    Status m_origin;
    GeoDataPlacemark m_placemarkA;
    GeoDataPlacemark m_placemarkB;
};

class Q_DECL_HIDDEN BookmarkSyncManager::Private
{
public:
    Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager);

    BookmarkSyncManager *const m_q;
    CloudSyncManager *const m_cloudSyncManager;

    QNetworkAccessManager m_network;
    QString m_uploadEndpoint;
    QString m_downloadEndpoint;
    QString m_timestampEndpoint;

    QNetworkReply *m_uploadReply;
    QNetworkReply *m_downloadReply;
    QNetworkReply *m_timestampReply;

    QString m_cloudTimestamp;

    QString m_cachePath;
    QString m_localBookmarksPath;
    QString m_bookmarksTimestamp;

    QList<DiffItem> m_diffA;
    QList<DiffItem> m_diffB;
    QList<DiffItem> m_merged;
    DiffItem m_conflictItem;

    BookmarkManager *m_bookmarkManager = nullptr;
    QTimer m_syncTimer;
    bool m_bookmarkSyncEnabled;

    /**
     * Returns an API endpoint
     * @param endpoint Endpoint itself without server info
     * @return Complete API URL as QUrl
     */
    QUrl endpointUrl(const QString &endpoint) const;

    /**
     * Uploads local bookmarks.kml to cloud.
     */
    void uploadBookmarks();

    /**
     * Downloads bookmarks.kml from cloud.
     */
    void downloadBookmarks();

    /**
     * Gets cloud bookmarks.kml's timestamp from cloud.
     */
    void downloadTimestamp();

    /**
     * Compares cloud bookmarks.kml's timestamp to last synced bookmarks.kml's timestamp.
     * @return true if cloud one is different from last synced one.
     */
    bool cloudBookmarksModified(const QString &cloudTimestamp) const;

    /**
     * Removes all KMLs in the cache except the
     * one with youngest timestamp.
     */
    void clearCache();

    /**
     * Finds the last synced bookmarks.kml file and returns its path
     * @return Path of last synced bookmarks.kml file.
     */
    QString lastSyncedKmlPath() const;

    /**
     * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list.
     * @param document The document whose placemarks will be compared to another document's placemarks.
     * @param other The document whose placemarks will be compared to the first document's placemarks.
     * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination.
     * @return A list of DiffItems
     */
    QList<DiffItem> getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection);

    /**
     * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list.
     * @param folder The folder whose placemarks will be compared to another document's placemarks.
     * @param path Path of the folder.
     * @param other The document whose placemarks will be compared to the first document's placemarks.
     * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination.
     * @return A list of DiffItems
     */
    QList<DiffItem> getPlacemarks(GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection);

    /**
     * Finds the placemark which has the same coordinates with given bookmark
     * @param container Container of placemarks which will be compared. Can be document or folder.
     * @param bookmark The bookmark whose counterpart will be searched in the container.
     * @return Counterpart of the given placemark.
     */
    const GeoDataPlacemark *findPlacemark(GeoDataContainer *container, const GeoDataPlacemark &bookmark) const;

    /**
     * Determines the status (created, deleted, changed or unchanged) of given DiffItem
     * by comparing the item's placemark with placemarks of given GeoDataDocument.
     * @param item The item whose status will be determined.
     * @param document The document whose placemarks will be used to determine DiffItem's status.
     */
    void determineDiffStatus(DiffItem &item, GeoDataDocument *document) const;

    /**
     * Finds differences between two bookmark files.
     * @param sourcePath Source bookmark
     * @param destinationPath Destination bookmark
     * @return A list of differences
     */
    QList<DiffItem> diff(QString &sourcePath, QString &destinationPath);
    QList<DiffItem> diff(QString &sourcePath, QIODevice *destination);
    QList<DiffItem> diff(QIODevice *source, QString &destinationPath);
    QList<DiffItem> diff(QIODevice *source, QIODevice *destination);

    /**
     * Merges two diff lists.
     * @param diffListA First diff list.
     * @param diffListB Second diff list.
     * @return Merged DiffItems.
     */
    void merge();

    /**
     * Creates GeoDataFolders using strings in path list.
     * @param container Container which created GeoDataFolder will be attached to.
     * @param pathList Names of folders. Note that each item will be the child of the previous one.
     * @return A pointer to created folder.
     */
    GeoDataFolder *createFolders(GeoDataContainer *container, QStringList &pathList);

    /**
     * Creates a GeoDataDocument using a list of DiffItems.
     * @param mergedList DiffItems which will be used as placemarks.
     * @return A pointer to created document.
     */
    GeoDataDocument *constructDocument(const QList<DiffItem> &mergedList);

    void saveDownloadedToCache(const QByteArray &kml);

    void parseTimestamp();
    void copyLocalToCache();

    void continueSynchronization();
    void completeSynchronization();
    void completeMerge();
    void completeUpload();
};

BookmarkSyncManager::Private::Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager)
    : m_q(parent)
    , m_cloudSyncManager(cloudSyncManager)
    , m_bookmarkManager(nullptr)
    , m_bookmarkSyncEnabled(false)
{
    m_cachePath = MarbleDirs::localPath() + QLatin1StringView("/cloudsync/cache/bookmarks");
    m_localBookmarksPath = MarbleDirs::localPath() + QLatin1StringView("/bookmarks/bookmarks.kml");
    m_downloadEndpoint = QStringLiteral("bookmarks/kml");
    m_uploadEndpoint = QStringLiteral("bookmarks/update");
    m_timestampEndpoint = QStringLiteral("bookmarks/timestamp");
}

BookmarkSyncManager::BookmarkSyncManager(CloudSyncManager *cloudSyncManager)
    : QObject()
    , d(new Private(this, cloudSyncManager))
{
    d->m_syncTimer.setInterval(60 * 60 * 1000); // 1 hour. TODO: Make this configurable.
    connect(&d->m_syncTimer, &QTimer::timeout, this, &BookmarkSyncManager::startBookmarkSync);
}

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

QDateTime BookmarkSyncManager::lastSync() const
{
    const QString last = d->lastSyncedKmlPath();
    if (last.isEmpty())
        return {};
    return QFileInfo(last).metadataChangeTime();
}

bool BookmarkSyncManager::isBookmarkSyncEnabled() const
{
    return d->m_bookmarkSyncEnabled;
}

void BookmarkSyncManager::setBookmarkSyncEnabled(bool enabled)
{
    bool const old_state = isBookmarkSyncEnabled();
    d->m_bookmarkSyncEnabled = enabled;
    if (old_state != isBookmarkSyncEnabled()) {
        Q_EMIT bookmarkSyncEnabledChanged(d->m_bookmarkSyncEnabled);
        if (isBookmarkSyncEnabled()) {
            startBookmarkSync();
        }
    }
}

void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager)
{
    d->m_bookmarkManager = manager;
    connect(manager, SIGNAL(bookmarksChanged()), this, SLOT(startBookmarkSync()));
    startBookmarkSync();
}

void BookmarkSyncManager::startBookmarkSync()
{
    if (!d->m_cloudSyncManager->isSyncEnabled() || !isBookmarkSyncEnabled()) {
        return;
    }

    d->m_syncTimer.start();
    d->downloadTimestamp();
}

QUrl BookmarkSyncManager::Private::endpointUrl(const QString &endpoint) const
{
    return QUrl(m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint);
}

void BookmarkSyncManager::Private::uploadBookmarks()
{
    QByteArray data;
    QByteArray lineBreak = "\r\n";
    QString word = QStringLiteral("----MarbleCloudBoundary");
    QString boundary = QStringLiteral("--%0").arg(word);
    QNetworkRequest request(endpointUrl(m_uploadEndpoint));
    request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data; boundary=%0").arg(word));

    data.append(QString(boundary + lineBreak).toUtf8());
    data.append(R"(Content-Disposition: form-data; name="bookmarks"; filename="bookmarks.kml")" + lineBreak);
    data.append("Content-Type: application/vnd.google-earth.kml+xml" + lineBreak + lineBreak);

    QFile bookmarksFile(m_localBookmarksPath);
    if (!bookmarksFile.open(QFile::ReadOnly)) {
        mDebug() << "Failed to open file" << bookmarksFile.fileName() << ". It is either missing or not readable.";
        return;
    }

    QByteArray kmlContent = bookmarksFile.readAll();
    data.append(kmlContent + lineBreak + lineBreak);
    data.append(QString(boundary).toUtf8());
    bookmarksFile.close();

    m_uploadReply = m_network.post(request, data);
    connect(m_uploadReply, SIGNAL(uploadProgress(qint64, qint64)), m_q, SIGNAL(uploadProgress(qint64, qint64)));
    connect(m_uploadReply, SIGNAL(finished()), m_q, SLOT(completeUpload()));
}

void BookmarkSyncManager::Private::downloadBookmarks()
{
    QNetworkRequest request(endpointUrl(m_downloadEndpoint));
    m_downloadReply = m_network.get(request);
    connect(m_downloadReply, SIGNAL(finished()), m_q, SLOT(completeSynchronization()));
    connect(m_downloadReply, SIGNAL(downloadProgress(qint64, qint64)), m_q, SIGNAL(downloadProgress(qint64, qint64)));
}

void BookmarkSyncManager::Private::downloadTimestamp()
{
    mDebug() << "Determining remote bookmark state.";
    m_timestampReply = m_network.get(QNetworkRequest(endpointUrl(m_timestampEndpoint)));
    connect(m_timestampReply, SIGNAL(finished()), m_q, SLOT(parseTimestamp()));
}

bool BookmarkSyncManager::Private::cloudBookmarksModified(const QString &cloudTimestamp) const
{
    QStringList entryList = QDir(m_cachePath)
                                .entryList(
                                    // TODO: replace with regex filter that only
                                    // allows timestamp filenames
                                    QStringList() << QStringLiteral("*.kml"),
                                    QDir::NoFilter,
                                    QDir::Name);
    if (!entryList.isEmpty()) {
        QString lastSynced = entryList.last();
        lastSynced.chop(4);
        return cloudTimestamp != lastSynced;
    } else {
        return true; // That will let cloud one get downloaded.
    }
}

void BookmarkSyncManager::Private::clearCache()
{
    QDir cacheDir(m_cachePath);
    QFileInfoList fileInfoList = cacheDir.entryInfoList(QStringList() << QStringLiteral("*.kml"), QDir::NoFilter, QDir::Name);
    if (!fileInfoList.isEmpty()) {
        for (const QFileInfo &fileInfo : std::as_const(fileInfoList)) {
            QFile file(fileInfo.absoluteFilePath());
            bool removed = file.remove();
            if (!removed) {
                mDebug() << "Could not delete" << file.fileName() << "Make sure you have sufficient permissions.";
            }
        }
    }
}

QString BookmarkSyncManager::Private::lastSyncedKmlPath() const
{
    QDir cacheDir(m_cachePath);
    QFileInfoList fileInfoList = cacheDir.entryInfoList(QStringList() << QStringLiteral("*.kml"), QDir::NoFilter, QDir::Name);
    if (!fileInfoList.isEmpty()) {
        return fileInfoList.last().absoluteFilePath();
    } else {
        return {};
    }
}

QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection)
{
    QList<DiffItem> diffItems;
    const auto folders = document->folderList();
    for (GeoDataFolder *folder : folders) {
        QString path = QStringLiteral("/%0").arg(folder->name());
        diffItems.append(getPlacemarks(folder, path, other, diffDirection));
    }

    return diffItems;
}

QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks(GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection)
{
    QList<DiffItem> diffItems;
    const auto folders = folder->folderList();
    for (GeoDataFolder *subFolder : folders) {
        QString newPath = QStringLiteral("%0/%1").arg(path, subFolder->name());
        diffItems.append(getPlacemarks(subFolder, newPath, other, diffDirection));
    }

    const auto placemarks = folder->placemarkList();
    for (GeoDataPlacemark *placemark : placemarks) {
        DiffItem diffItem;
        diffItem.m_path = path;
        diffItem.m_placemarkA = *placemark;
        switch (diffDirection) {
        case DiffItem::Source:
            diffItem.m_origin = DiffItem::Destination;
            break;
        case DiffItem::Destination:
            diffItem.m_origin = DiffItem::Source;
            break;
        default:
            break;
        }

        determineDiffStatus(diffItem, other);

        if (!(diffItem.m_action == DiffItem::NoAction && diffItem.m_origin == DiffItem::Destination)
            && !(diffItem.m_action == DiffItem::Changed && diffItem.m_origin == DiffItem::Source)) {
            diffItems.append(diffItem);
        }
    }

    return diffItems;
}

const GeoDataPlacemark *BookmarkSyncManager::Private::findPlacemark(GeoDataContainer *container, const GeoDataPlacemark &bookmark) const
{
    const auto placemarks = container->placemarkList();
    for (GeoDataPlacemark *placemark : placemarks) {
        if (EARTH_RADIUS * placemark->coordinate().sphericalDistanceTo(bookmark.coordinate()) <= 1) {
            return placemark;
        }
    }

    const auto folders = container->folderList();
    for (GeoDataFolder *folder : folders) {
        const GeoDataPlacemark *placemark = findPlacemark(folder, bookmark);
        if (placemark) {
            return placemark;
        }
    }

    return nullptr;
}

void BookmarkSyncManager::Private::determineDiffStatus(DiffItem &item, GeoDataDocument *document) const
{
    const GeoDataPlacemark *match = findPlacemark(document, item.m_placemarkA);

    if (match != nullptr) {
        item.m_placemarkB = *match;
        bool nameChanged = item.m_placemarkA.name() != item.m_placemarkB.name();
        bool descChanged = item.m_placemarkA.description() != item.m_placemarkB.description();
        bool lookAtChanged = item.m_placemarkA.lookAt()->latitude() != item.m_placemarkB.lookAt()->latitude()
            || item.m_placemarkA.lookAt()->longitude() != item.m_placemarkB.lookAt()->longitude()
            || item.m_placemarkA.lookAt()->altitude() != item.m_placemarkB.lookAt()->altitude()
            || item.m_placemarkA.lookAt()->range() != item.m_placemarkB.lookAt()->range();
        if (nameChanged || descChanged || lookAtChanged) {
            item.m_action = DiffItem::Changed;
        } else {
            item.m_action = DiffItem::NoAction;
        }
    } else {
        switch (item.m_origin) {
        case DiffItem::Source:
            item.m_action = DiffItem::Deleted;
            item.m_placemarkB = item.m_placemarkA; // for conflict purposes
            break;
        case DiffItem::Destination:
            item.m_action = DiffItem::Created;
            break;
        }
    }
}

QList<DiffItem> BookmarkSyncManager::Private::diff(QString &sourcePath, QString &destinationPath)
{
    QFile fileB(destinationPath);
    if (!fileB.open(QFile::ReadOnly)) {
        mDebug() << "Could not open file " << fileB.fileName();
    }
    return diff(sourcePath, &fileB);
}

QList<DiffItem> BookmarkSyncManager::Private::diff(QString &sourcePath, QIODevice *fileB)
{
    QFile fileA(sourcePath);
    if (!fileA.open(QFile::ReadOnly)) {
        mDebug() << "Could not open file " << fileA.fileName();
    }

    return diff(&fileA, fileB);
}

QList<DiffItem> BookmarkSyncManager::Private::diff(QIODevice *source, QString &destinationPath)
{
    QFile fileB(destinationPath);
    if (!fileB.open(QFile::ReadOnly)) {
        mDebug() << "Could not open file " << fileB.fileName();
    }

    return diff(source, &fileB);
}

QList<DiffItem> BookmarkSyncManager::Private::diff(QIODevice *fileA, QIODevice *fileB)
{
    GeoDataParser parserA(GeoData_KML);
    parserA.read(fileA);
    auto documentA = dynamic_cast<GeoDataDocument *>(parserA.releaseDocument());

    GeoDataParser parserB(GeoData_KML);
    parserB.read(fileB);
    auto documentB = dynamic_cast<GeoDataDocument *>(parserB.releaseDocument());

    QList<DiffItem> diffItems = getPlacemarks(documentA, documentB, DiffItem::Destination); // Compare old to new
    diffItems.append(getPlacemarks(documentB, documentA, DiffItem::Source)); // Compare new to old

    // Compare paths
    for (int i = 0; i < diffItems.count(); i++) {
        for (int p = i + 1; p < diffItems.count(); p++) {
            if ((diffItems[i].m_origin == DiffItem::Source) && (diffItems[i].m_action == DiffItem::NoAction)
                && (EARTH_RADIUS * diffItems[i].m_placemarkA.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkB.coordinate()) <= 1)
                && (EARTH_RADIUS * diffItems[i].m_placemarkB.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkA.coordinate()) <= 1)
                && (diffItems[i].m_path != diffItems[p].m_path)) {
                diffItems[p].m_action = DiffItem::Changed;
            }
        }
    }

    return diffItems;
}

void BookmarkSyncManager::Private::merge()
{
    for (const DiffItem &itemA : std::as_const(m_diffA)) {
        if (itemA.m_action == DiffItem::NoAction) {
            bool deleted = false;
            bool changed = false;
            DiffItem other;

            for (const DiffItem &itemB : std::as_const(m_diffB)) {
                if (EARTH_RADIUS * itemA.m_placemarkA.coordinate().sphericalDistanceTo(itemB.m_placemarkA.coordinate()) <= 1) {
                    if (itemB.m_action == DiffItem::Deleted) {
                        deleted = true;
                    } else if (itemB.m_action == DiffItem::Changed) {
                        changed = true;
                        other = itemB;
                    }
                }
            }
            if (changed) {
                m_merged.append(other);
            } else if (!deleted) {
                m_merged.append(itemA);
            }
        } else if (itemA.m_action == DiffItem::Created) {
            m_merged.append(itemA);
        } else if (itemA.m_action == DiffItem::Changed || itemA.m_action == DiffItem::Deleted) {
            bool conflict = false;
            DiffItem other;

            for (const DiffItem &itemB : std::as_const(m_diffB)) {
                if (EARTH_RADIUS * itemA.m_placemarkB.coordinate().sphericalDistanceTo(itemB.m_placemarkB.coordinate()) <= 1) {
                    if ((itemA.m_action == DiffItem::Changed && (itemB.m_action == DiffItem::Changed || itemB.m_action == DiffItem::Deleted))
                        || (itemA.m_action == DiffItem::Deleted && itemB.m_action == DiffItem::Changed)) {
                        conflict = true;
                        other = itemB;
                    }
                }
            }

            if (!conflict && itemA.m_action == DiffItem::Changed) {
                m_merged.append(itemA);
            } else if (conflict) {
                m_conflictItem = other;
                auto mergeItem = new MergeItem();
                mergeItem->setPathA(itemA.m_path);
                mergeItem->setPathB(other.m_path);
                mergeItem->setPlacemarkA(itemA.m_placemarkA);
                mergeItem->setPlacemarkB(other.m_placemarkA);

                switch (itemA.m_action) {
                case DiffItem::Changed:
                    mergeItem->setActionA(MergeItem::Changed);
                    break;
                case DiffItem::Deleted:
                    mergeItem->setActionA(MergeItem::Deleted);
                    break;
                default:
                    break;
                }

                switch (other.m_action) {
                case DiffItem::Changed:
                    mergeItem->setActionB(MergeItem::Changed);
                    break;
                case DiffItem::Deleted:
                    mergeItem->setActionB(MergeItem::Deleted);
                    break;
                default:
                    break;
                }

                Q_EMIT m_q->mergeConflict(mergeItem);
                return;
            }
        }

        if (!m_diffA.isEmpty()) {
            m_diffA.removeFirst();
        }
    }

    for (const DiffItem &itemB : std::as_const(m_diffB)) {
        if (itemB.m_action == DiffItem::Created) {
            m_merged.append(itemB);
        }
    }

    completeMerge();
}

GeoDataFolder *BookmarkSyncManager::Private::createFolders(GeoDataContainer *container, QStringList &pathList)
{
    GeoDataFolder *folder = nullptr;
    if (pathList.count() > 0) {
        const QString name = pathList.takeFirst();

        const auto otherFolders = container->folderList();
        for (GeoDataFolder *otherFolder : otherFolders) {
            if (otherFolder->name() == name) {
                folder = otherFolder;
            }
        }

        if (folder == nullptr) {
            folder = new GeoDataFolder();
            folder->setName(name);
            container->append(folder);
        }

        if (pathList.isEmpty()) {
            return folder;
        }
    }

    return createFolders(folder, pathList);
}

GeoDataDocument *BookmarkSyncManager::Private::constructDocument(const QList<DiffItem> &mergedList)
{
    auto document = new GeoDataDocument();
    document->setName(tr("Bookmarks"));

    for (const DiffItem &item : mergedList) {
        auto placemark = new GeoDataPlacemark(item.m_placemarkA);
        QStringList splitten = item.m_path.split(QLatin1Char('/'), Qt::SkipEmptyParts);
        GeoDataFolder *folder = createFolders(document, splitten);
        folder->append(placemark);
    }

    return document;
}

void BookmarkSyncManager::resolveConflict(MergeItem *item)
{
    DiffItem diffItem;

    switch (item->resolution()) {
    case MergeItem::A:
        if (!d->m_diffA.isEmpty()) {
            diffItem = d->m_diffA.first();
            break;
        }
        Q_FALLTHROUGH();
    case MergeItem::B:
        diffItem = d->m_conflictItem;
        break;
    default:
        return; // It shouldn't happen.
    }

    if (diffItem.m_action != DiffItem::Deleted) {
        d->m_merged.append(diffItem);
    }

    if (!d->m_diffA.isEmpty()) {
        d->m_diffA.removeFirst();
    }

    d->merge();
}

void BookmarkSyncManager::Private::saveDownloadedToCache(const QByteArray &kml)
{
    QString localBookmarksDir = m_localBookmarksPath;
    QDir().mkdir(localBookmarksDir.remove(QStringLiteral("bookmarks.kml")));
    QFile bookmarksFile(m_localBookmarksPath);
    if (!bookmarksFile.open(QFile::ReadWrite)) {
        mDebug() << "Failed to open file" << bookmarksFile.fileName() << ". It is either missing or not readable.";
        return;
    }

    bookmarksFile.write(kml);
    bookmarksFile.close();
    copyLocalToCache();
}

void BookmarkSyncManager::Private::parseTimestamp()
{
    QJsonDocument jsonDoc = QJsonDocument::fromJson(m_timestampReply->readAll());
    QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));

    m_cloudTimestamp = dataValue.toString();
    mDebug() << "Remote bookmark timestamp is " << m_cloudTimestamp;
    continueSynchronization();
}
void BookmarkSyncManager::Private::copyLocalToCache()
{
    QDir().mkpath(m_cachePath);
    clearCache();

    QFile bookmarksFile(m_localBookmarksPath);
    bookmarksFile.copy(QStringLiteral("%0/%1.kml").arg(m_cachePath, m_cloudTimestamp));
}

// Bookmark synchronization steps
void BookmarkSyncManager::Private::continueSynchronization()
{
    bool cloudModified = cloudBookmarksModified(m_cloudTimestamp);
    if (cloudModified) {
        downloadBookmarks();
    } else {
        QString lastSyncedPath = lastSyncedKmlPath();
        if (lastSyncedPath.isEmpty()) {
            mDebug() << "Never synced. Uploading bookmarks.";
            uploadBookmarks();
        } else {
            QList<DiffItem> diffList = diff(lastSyncedPath, m_localBookmarksPath);
            bool localModified = false;
            for (const DiffItem &item : std::as_const(diffList)) {
                if (item.m_action != DiffItem::NoAction) {
                    localModified = true;
                }
            }

            if (localModified) {
                mDebug() << "Local modifications, uploading.";
                uploadBookmarks();
            }
        }
    }
}

void BookmarkSyncManager::Private::completeSynchronization()
{
    mDebug() << "Merging remote and local bookmark file";
    QString lastSyncedPath = lastSyncedKmlPath();
    QFile localBookmarksFile(m_localBookmarksPath);
    QByteArray result = m_downloadReply->readAll();
    QBuffer buffer(&result);
    buffer.open(QIODevice::ReadOnly);

    if (lastSyncedPath.isEmpty()) {
        if (localBookmarksFile.exists()) {
            mDebug() << "Conflict between remote bookmarks and local ones";
            m_diffA = diff(&buffer, m_localBookmarksPath);
            m_diffB = diff(m_localBookmarksPath, &buffer);
        } else {
            saveDownloadedToCache(result);
            return;
        }
    } else {
        m_diffA = diff(lastSyncedPath, m_localBookmarksPath);
        m_diffB = diff(lastSyncedPath, &buffer);
    }

    m_merged.clear();
    merge();
}

void BookmarkSyncManager::Private::completeMerge()
{
    QFile localBookmarksFile(m_localBookmarksPath);
    GeoDataDocument *doc = constructDocument(m_merged);
    GeoWriter writer;
    localBookmarksFile.remove();
    localBookmarksFile.open(QFile::ReadWrite);
    writer.write(&localBookmarksFile, doc);
    localBookmarksFile.close();
    uploadBookmarks();
}

void BookmarkSyncManager::Private::completeUpload()
{
    QJsonDocument jsonDoc = QJsonDocument::fromJson(m_uploadReply->readAll());
    QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));

    m_cloudTimestamp = dataValue.toString();
    mDebug() << "Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp;
    copyLocalToCache();
    Q_EMIT m_q->syncComplete();
}

}

#include "moc_BookmarkSyncManager.cpp"
