/*
 * Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
 * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
 * Copyright (C) by Klaas Freitag <freitag@owncloud.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.
 */

#include "folder.h"

#include "account.h"
#include "accountmanager.h"
#include "accountstate.h"
#include "application.h"
#include "common/checksums.h"
#include "common/filesystembase.h"
#include "common/syncjournalfilerecord.h"
#include "common/version.h"
#include "common/vfs.h"
#include "configfile.h"
#include "filesystem.h"
#include "folderman.h"
#include "foldermanagement/foldermanagementutils.h"
#include "folderwatcher.h"
#include "libsync/graphapi/spacesmanager.h"
#include "localdiscoverytracker.h"
#include "scheduling/syncscheduler.h"
#include "settingsdialog.h"
#include "socketapi/socketapi.h"
#include "syncengine.h"
#include "syncresult.h"
#include "syncrunfilelog.h"
#include "theme.h"

#ifdef Q_OS_WIN
#include "common/utility_win.h"
#endif

#include <QTimer>
#include <QUrl>
#include <QDir>
#include <QSettings>

#include <QMessageBox>

using namespace std::chrono_literals;

namespace {
/* How often to retry a sync
 * Either due to _engine->isAnotherSyncNeeded or a sync error
 */
constexpr int retrySyncLimitC = 3;

auto davUrlC()
{
    return QStringLiteral("davUrl");
}

auto spaceIdC()
{
    return QStringLiteral("spaceId");
}

auto displayNameC()
{
    return QLatin1String("displayString");
}

// todo #52 - eliminate this config value and scrub configs next major release
[[deprecated("deployed concept is no longer supported and will be removed in client 8.0")]] auto deployedC()
{
    return QStringLiteral("deployed");
}

auto priorityC()
{
    return QStringLiteral("priority");
}
}

namespace OCC {

using namespace FileSystem::SizeLiterals;

Q_LOGGING_CATEGORY(lcFolder, "gui.folder", QtInfoMsg)

Folder::Folder(const FolderDefinition &definition, AccountState *accountState, SyncJournalDb *journal, Vfs *vfs, SyncEngine *engine, QObject *parent)
    : QObject(parent)
    , _accountState(accountState)
    , _definition(definition)
    , _fileLog(new SyncRunFileLog)
{
    // reparent the injected dependencies and set them as members
    journal->setParent(this);
    _journal = journal;

    vfs->setParent(this);
    _vfs = vfs;

    engine->setParent(this);
    _engine = engine;

    // the FolderBuilder should fail under all of the following conditions, so none of thes asserts should ever trigger
    Q_ASSERT(_accountState && _accountState->account());
    Q_ASSERT(_vfs);
    Q_ASSERT(_journal);
    Q_ASSERT(_engine);

    _timeSinceLastSyncStart.start();
    _timeSinceLastSyncDone.start();

    SyncResult::Status status = SyncResult::NotYetStarted;
    if (definition.paused()) {
        status = SyncResult::Paused;
    }
    setSyncState(status);

    connect(_accountState, &AccountState::isConnectedChanged, this, &Folder::canSyncChanged);

    // connect engine to folderman:
    connect(_engine, &SyncEngine::seenLockedFile, FolderMan::instance(), &FolderMan::slotSyncOnceFileUnlocks);

    // connect progress dispatcher to this
    connect(ProgressDispatcher::instance(), &ProgressDispatcher::folderConflicts, this, &Folder::slotFolderConflicts);

    // connect engine to this:
    connect(_engine, &SyncEngine::started, this, &Folder::slotSyncStarted, Qt::QueuedConnection);
    connect(_engine, &SyncEngine::finished, this, &Folder::slotSyncFinished, Qt::QueuedConnection);

    connect(
        _engine, &SyncEngine::transmissionProgress, this, [this](const ProgressInfo &pi) { Q_EMIT ProgressDispatcher::instance()->progressInfo(this, pi); });

    connect(_engine, &SyncEngine::transmissionProgress, this, &Folder::progressUpdate);
    connect(_engine, &SyncEngine::itemCompleted, this, &Folder::slotItemCompleted);
    connect(_engine, &SyncEngine::aboutToPropagate, this, &Folder::slotLogPropagationStart);
    connect(_engine, &SyncEngine::syncError, this, &Folder::slotSyncError);
    connect(_engine, &SyncEngine::excluded, this, [this](const QString &path) { Q_EMIT ProgressDispatcher::instance()->excluded(this, path); });

    // setup local discovery tracker
    _localDiscoveryTracker = new LocalDiscoveryTracker(this);
    connect(_engine, &SyncEngine::finished, _localDiscoveryTracker, &LocalDiscoveryTracker::slotSyncFinished);
    connect(_engine, &SyncEngine::itemCompleted, _localDiscoveryTracker, &LocalDiscoveryTracker::slotItemCompleted);

    // this needs investigation as it looks sus. I would expect to get a signal like this from the space directly
    connect(_accountState->account()->spacesManager(), &GraphApi::SpacesManager::spaceChanged, this, [this](GraphApi::Space *changedSpace) {
        if (_definition.spaceId() == changedSpace->id()) {
            emit spaceChanged();
        }
    });

    if (space())
        emit spaceChanged();

    // Initialize the vfs plugin. Do this after the UI is running, so we can show a dialog when something goes wrong.
    QTimer::singleShot(0, this, &Folder::startVfs);
}

Folder::~Folder()
{
    // If wipeForRemoval() was called vfs has already shut down, and the journal has been closed.
    // any sync the engine was running should have been paused/aborted in folderman as prep to remove the folder.
    // this feels very sketchy to me and I'm not really sure we should outsource all of that responsibility!
    // I think the main question is whether the child cleanup routines can finish in time before the pointers are deleted.
    // this is especially true for sync engine (if it's running on dtr) but any of them could get humg up, in theory
    // discuss with cohorts.
    if (_vfs)
        _vfs->stop();
    if (_engine)
        Q_ASSERT(!_engine->isSyncRunning());
    if (_journal)
        _journal->close();
}

GraphApi::Space *Folder::space() const
{
    if (_accountState && _accountState->account() && _accountState->account()->spacesManager()) {
        return _accountState->account()->spacesManager()->space(_definition.spaceId());
    }
    return nullptr;
}

SyncOptions Folder::loadSyncOptions()
{
    if (!_accountState || !_accountState->account() || !_vfs)
        return SyncOptions();

    SyncOptions opt(_vfs);
    opt._parallelNetworkJobs = _accountState->account()->isHttp2Supported() ? 20 : 6;

    // apparently the env can override the default
    int maxParallel = qEnvironmentVariableIntValue("OWNCLOUD_MAX_PARALLEL");
    if (maxParallel > 0)
        opt._parallelNetworkJobs = maxParallel;

    return opt;
}

QString Folder::displayName() const
{
    if (auto *s = space()) {
        return s->displayName();
    }
    return _definition.displayName();
}


QString Folder::shortGuiLocalPath() const
{
    QString p = _definition.localPath();
    QString home = QDir::homePath();
    if (!home.endsWith(QLatin1Char('/'))) {
        home.append(QLatin1Char('/'));
    }
    if (p.startsWith(home)) {
        p = p.mid(home.length());
    }
    if (p.length() > 1 && p.endsWith(QLatin1Char('/'))) {
        p.chop(1);
    }
    return QDir::toNativeSeparators(p);
}


QString Folder::cleanPath() const
{
    QString cleanedPath = QDir::cleanPath(_definition.canonicalPath());

    if (cleanedPath.length() == 3 && cleanedPath.endsWith(QLatin1String(":/")))
        cleanedPath.remove(2, 1);

    return cleanedPath;
}

QUrl Folder::webDavUrl() const
{
    // this doesn't make sense to me - the definition should always carry the correct webdav url so I'm
    // changing this to a reality check against the space instead of relying on the space value first, if it exists,
    GraphApi::Space *sp = space();
    if (sp) {
        QUrl spUrl = sp->webDavUrl();
        Q_ASSERT(spUrl == _definition.webDavUrl());
    }
    return _definition.webDavUrl();
}

QString Folder::remotePathTrailingSlash() const
{
    const QString remote = remotePath();
    if (!remote.endsWith((QLatin1Char('/'))))
        return remote + QLatin1Char('/');
    return remote;
}

bool Folder::isSyncRunning() const
{
    return !hasSetupError() && _engine->isSyncRunning();
}

bool Folder::canSync() const
{
    if (!_engine || !_accountState || !_accountState->account() || !_folderWatcher)
        return false;
    return isAvailable() && !syncPaused() && _accountState->readyForSync() && isReady() && _accountState->account()->hasCapabilities();
}

bool Folder::isReady() const
{
    return _vfsIsReady;
}

void Folder::setSyncPaused(bool paused)
{
    if (hasSetupError()) {
        return;
    }
    if (paused == _definition.paused()) {
        return;
    }

    _definition.setPaused(paused);

    Q_EMIT syncPausedChanged(this, paused);
    if (!paused) {
        setSyncState(SyncResult::NotYetStarted);
    } else {
        setSyncState(SyncResult::Paused);
    }
    Q_EMIT canSyncChanged();
}

void Folder::setAvailable(bool available)
{
    if (available != (space() != nullptr))
        return;

    _available = available;
    _syncResult.reset();
    if (!_available) {
        _syncResult.setStatus(SyncResult::Status::Unavailable);
        _syncResult.appendErrorString(tr("The folder has been disabled or removed from the server"));
    } else {
        _syncResult.setStatus(SyncResult::Status::NotYetStarted);
    }

    emit syncStateChange();
    emit canSyncChanged();
}

bool Folder::isConnected()
{
    return (_accountState && _accountState->isConnected());
}

void Folder::setSyncState(SyncResult::Status state)
{
    if (state != _syncResult.status()) {
        _syncResult.setStatus(state);
        Q_EMIT syncStateChange();
    }
}


void Folder::showSyncResultPopup()
{
    if (_syncResult.firstItemNew()) {
        createGuiLog(_syncResult.firstItemNew()->destination(), LogStatusNew, _syncResult.numNewItems());
    }
    if (_syncResult.firstItemDeleted()) {
        createGuiLog(_syncResult.firstItemDeleted()->destination(), LogStatusRemove, _syncResult.numRemovedItems());
    }
    if (_syncResult.firstItemUpdated()) {
        createGuiLog(_syncResult.firstItemUpdated()->destination(), LogStatusUpdated, _syncResult.numUpdatedItems());
    }

    if (_syncResult.firstItemRenamed()) {
        LogStatus status(LogStatusRename);
        // if the path changes it's rather a move
        QDir renTarget = QFileInfo(_syncResult.firstItemRenamed()->_renameTarget).dir();
        QDir renSource = QFileInfo(_syncResult.firstItemRenamed()->_file).dir();
        if (renTarget != renSource) {
            status = LogStatusMove;
        }
        createGuiLog(_syncResult.firstItemRenamed()->_file, status,
            _syncResult.numRenamedItems(), _syncResult.firstItemRenamed()->_renameTarget);
    }

    if (_syncResult.firstNewConflictItem()) {
        createGuiLog(_syncResult.firstNewConflictItem()->destination(), LogStatusConflict, _syncResult.numNewConflictItems());
    }
    if (int errorCount = _syncResult.numErrorItems()) {
        createGuiLog(_syncResult.firstItemError()->_file, LogStatusError, errorCount);
    }

    qCInfo(lcFolder) << "Folder" << path() << "sync result: " << _syncResult.status();
}

void Folder::createGuiLog(const QString &filename, LogStatus status, int count,
    const QString &renameTarget)
{
    if (count > 0) {
        QString file = QDir::toNativeSeparators(filename);
        QString text;

        switch (status) {
        case LogStatusRemove:
            if (count > 1) {
                text = tr("%1 and %n other file(s) have been removed.", "", count - 1).arg(file);
            } else {
                text = tr("%1 has been removed.", "%1 names a file.").arg(file);
            }
            break;
        case LogStatusNew:
            if (count > 1) {
                text = tr("%1 and %n other file(s) have been added.", "", count - 1).arg(file);
            } else {
                text = tr("%1 has been added.", "%1 names a file.").arg(file);
            }
            break;
        case LogStatusUpdated:
            if (count > 1) {
                text = tr("%1 and %n other file(s) have been updated.", "", count - 1).arg(file);
            } else {
                text = tr("%1 has been updated.", "%1 names a file.").arg(file);
            }
            break;
        case LogStatusRename:
            if (count > 1) {
                text = tr("%1 has been renamed to %2 and %n other file(s) have been renamed.", "", count - 1).arg(file, renameTarget);
            } else {
                text = tr("%1 has been renamed to %2.", "%1 and %2 name files.").arg(file, renameTarget);
            }
            break;
        case LogStatusMove:
            if (count > 1) {
                text = tr("%1 has been moved to %2 and %n other file(s) have been moved.", "", count - 1).arg(file, renameTarget);
            } else {
                text = tr("%1 has been moved to %2.").arg(file, renameTarget);
            }
            break;
        case LogStatusConflict:
            if (count > 1) {
                text = tr("%1 and %n other file(s) have sync conflicts.", "", count - 1).arg(file);
            } else {
                text = tr("%1 has a sync conflict. Please check the conflict file!").arg(file);
            }
            break;
        case LogStatusError:
            if (count > 1) {
                text = tr("%1 and %n other file(s) could not be synced due to errors. See the log for details.", "", count - 1).arg(file);
            } else {
                text = tr("%1 could not be synced due to an error. See the log for details.").arg(file);
            }
            break;
        }

        if (!text.isEmpty()) {
            ocApp()->gui()->slotShowOptionalTrayMessage(tr("Sync Activity"), text);
        }
    }
}

void Folder::startVfs()
{
    if (!_accountState || !_accountState->account())
        return;

    OC_ENFORCE(_vfs);
    OC_ENFORCE(_vfs->mode() == _definition.virtualFilesMode());

    // todo: DC-219 check to see if this sync error makes it to the gui or not. We need to more clearly inform the user that they should
    // create a new sync connection to the remote folder, and use improved checks in the folder wizard to make sure vfs is supported
    // on the chosen path.
    // ideally we want a check like this before we even get here, eg in the area of load folder from config - but need to be sure there is
    // a place to clearly instruct the user that the folder sync needs to be re-built with a valid local path
    QString result = Vfs::pathSupportDetail(path(), _vfs->mode());
    if (!result.isEmpty()) {
        _syncResult.appendErrorString(result);
        setSyncState(SyncResult::SetupError);
        return;
    }

    VfsSetupParams vfsParams(_accountState->account(), webDavUrl(), _engine);
    vfsParams.filesystemPath = path();
    vfsParams.remotePath = remotePathTrailingSlash();
    vfsParams.journal = _journal;
    vfsParams.providerDisplayName = Theme::instance()->appNameGUI();
    vfsParams.providerName = Theme::instance()->appName();
    vfsParams.providerVersion = Version::version();
    vfsParams.multipleAccountsRegistered = AccountManager::instance()->accounts().size() > 1;

    connect(_engine, &SyncEngine::fileStatusChanged, _vfs, &Vfs::onFileStatusChanged);

    connect(_vfs, &Vfs::started, this, [this] {
        // Immediately mark the sqlite temporaries as excluded. They get recreated
        // on db-open and need to get marked again every time.
        QString stateDbFile = _journal->databaseFilePath();
        _vfs->onFileStatusChanged(stateDbFile + QStringLiteral("-wal"), SyncFileStatus::StatusExcluded);
        _vfs->onFileStatusChanged(stateDbFile + QStringLiteral("-shm"), SyncFileStatus::StatusExcluded);
        _engine->setSyncOptions(loadSyncOptions());
        registerFolderWatcher();

        connect(_vfs, &Vfs::needSync, this, [this] {
            if (canSync()) {
                // the vfs plugin detected that its metadata is out of sync and requests a new sync
                // the request has a high priority as it is probably issued after a user request
                FolderMan::instance()->scheduler()->enqueueFolder(this, SyncScheduler::Priority::High);
            }
        });
        _vfsIsReady = true;
        // We are set up, schedule ourselves if we can.
        // If not the scheduler will take care of it later.
        if (canSync()) {
            // Refactoring todo: also no. We need a general cleanup task to eval and fix all of these crossed triggers
            // the only element that should emit FolderMan signals is FolderMan! same for any/all other classes
            // I don't care if signals are now public: it is not correct for anyone but the owner to trigger them
            FolderMan::instance()->scheduler()->enqueueFolder(this);
        }
    });
    connect(_vfs, &Vfs::error, this, [this](const QString &error) {
        _syncResult.appendErrorString(error);
        setSyncState(SyncResult::SetupError);
        _vfsIsReady = false;
    });

    slotNextSyncFullLocalDiscovery();
    _vfs->start(vfsParams);
}

void Folder::slotDiscardDownloadProgress()
{
    // Delete from journal and from filesystem.
    QDir folderpath(_definition.localPath());
    QSet<QString> keep_nothing;
    const QVector<SyncJournalDb::DownloadInfo> deleted_infos = _journal->getAndDeleteStaleDownloadInfos(keep_nothing);
    for (const auto &deleted_info : deleted_infos) {
        const QString tmppath = folderpath.filePath(deleted_info._tmpfile);
        qCInfo(lcFolder) << "Deleting temporary file: " << tmppath;
        FileSystem::remove(tmppath);
    }
}

int Folder::slotWipeErrorBlacklist()
{
    return _journal->wipeErrorBlacklist();
}

void Folder::slotWatchedPathsChanged(const QSet<QString> &paths, ChangeReason reason)
{
    if (!isReady()) {
        // we might be switching backend
        return;
    }
    bool needSync = false;
    for (const auto &path : paths) {
        Q_ASSERT(FileSystem::isChildPathOf(path, this->path()));
        if (!FileSystem::isChildPathOf(path, this->path()))
            continue;
        const QString relativePath = path.mid(this->path().size());
        if (reason == ChangeReason::UnLock) {
            journalDb()->wipeErrorBlacklistEntry(relativePath, SyncJournalErrorBlacklistRecord::Category::LocalSoftError);

            {
                // horrible hack to compensate that we don't handle folder deletes on a per-file basis
                qsizetype index = 0;
                QString p = relativePath;
                while ((index = p.lastIndexOf(QLatin1Char('/'))) != -1) {
                    p = p.left(index);
                    const auto rec = journalDb()->errorBlacklistEntry(p);
                    if (rec.isValid()) {
                        if (rec._errorCategory == SyncJournalErrorBlacklistRecord::Category::LocalSoftError) {
                            journalDb()->wipeErrorBlacklistEntry(p);
                        }
                    }
                }
            }
        }

        // Add to list of locally modified paths
        //
        // We do this before checking for our own sync-related changes to make
        // extra sure to not miss relevant changes.
        _localDiscoveryTracker->addTouchedPath(relativePath);

        SyncJournalFileRecord record;
        _journal->getFileRecord(relativePath.toUtf8(), &record);
        if (reason != ChangeReason::UnLock) {
            // Check that the mtime/size actually changed or there was
            // an attribute change (pin state) that caused the notification
            bool spurious = false;
            if (record.isValid() && !FileSystem::fileChanged(QFileInfo{path}, record._fileSize, record._modtime, record._inode)) {
                spurious = true;

                if (auto pinState = _vfs->pinState(relativePath)) {
                    if (*pinState == PinState::AlwaysLocal && record.isVirtualFile())
                        spurious = false;
                    if (*pinState == PinState::OnlineOnly && record.isFile())
                        spurious = false;
                }
            }
            if (spurious) {
                qCInfo(lcFolder) << "Ignoring spurious notification for file" << relativePath;
                continue; // probably a spurious notification
            }
        }
        warnOnNewExcludedItem(record, relativePath);

        _engine->pathTouched(path);

        needSync = true;
    }
    if (needSync && canSync()) {
        FolderMan::instance()->scheduler()->enqueueFolder(this);
    }
}

void Folder::implicitlyHydrateFile(const QString &relativepath)
{
    qCInfo(lcFolder) << "Implicitly hydrate virtual file:" << relativepath;

    // Set in the database that we should download the file
    SyncJournalFileRecord record;
    _journal->getFileRecord(relativepath.toUtf8(), &record);
    if (!record.isValid()) {
        qCInfo(lcFolder) << "Did not find file in db";
        return;
    }
    if (!record.isVirtualFile()) {
        qCInfo(lcFolder) << "The file is not virtual";
        return;
    }
    record._type = ItemTypeVirtualFileDownload;
    _journal->setFileRecord(record);

    // Change the file's pin state if it's contradictory to being hydrated
    // (suffix-virtual file's pin state is stored at the hydrated path)
    const auto pin = _vfs->pinState(relativepath);
    if (pin && *pin == PinState::OnlineOnly) {
        std::ignore = _vfs->setPinState(relativepath, PinState::Unspecified);
    }

    // Add to local discovery
    schedulePathForLocalDiscovery(relativepath);
    FolderMan::instance()->scheduler()->enqueueFolder(this);
}

void Folder::setVirtualFilesEnabled(bool enabled)
{
    Vfs::Mode newMode = _definition.virtualFilesMode();
    if (enabled && _definition.virtualFilesMode() == Vfs::Off) {
        newMode = VfsPluginManager::instance().bestAvailableVfsMode();
    } else if (!enabled) {
        newMode = Vfs::Off;
    }
    if (newMode == _definition.virtualFilesMode()) {
        return;
    }
    if (isSyncRunning()) {
        connect(this, &Folder::syncFinished, this, [this, newMode] { changeVfsMode(newMode); }, Qt::SingleShotConnection);
        slotTerminateSync(tr("Switching VFS mode on folder '%1'").arg(displayName()));
    } else {
        changeVfsMode(newMode);
    }
}

// Refactoring todo: this still causes THREE saves to config file which is potentially excessive for large
// sets.
// I think the best way to fix it is to avoid calling setSyncPaused as that emits a couple of changes,
// which may be overkill for this temp setting change (ie running to paused and back to running)
// needs a deeper look. For the moment most of the pain related to this is avoided because we now only
// connect the folder after it's set up, so the saves during setup are avoided = huge improvement already.
void Folder::changeVfsMode(Vfs::Mode newMode)
{
    if (newMode == _definition.virtualFilesMode()) {
        return;
    }
    // if we can't create the new mode, just ditch.
    Vfs *newVfs = VfsPluginManager::instance().createVfsFromPlugin(newMode, this);
    if (!newVfs) {
        qCWarning(lcFolder) << "Unable to change vfs mode for Folder " << _definition.localPath() << " from " << _definition.virtualFilesMode() << " to "
                            << newMode << ". Leaving the original mode active.";
        return;
    }

    const bool wasPaused = _definition.paused();
    if (!wasPaused) {
        setSyncPaused(true);
    }

    // stash the previous blacklist
    bool ok;
    const auto oldBlacklist = journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, ok);

    if (!ok) {
        qCWarning(lcFolder) << "Unable to retrieve previous selective sync blacklist for folder: " << _definition.localPath();
        return;
    }
    // clear previous blacklist
    journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, {});


    // Wipe the dehydrated files from the DB, they will get downloaded on the next sync. We need to do this, otherwise the files
    // are in the DB but not on disk, so the client assumes they are deleted, and removes them from the remote.
    _vfs->wipeDehydratedVirtualFiles();

    // Tear down and disconnect the VFS
    _vfsIsReady = false;
    _vfs->stop();
    _vfs->unregisterFolder();
    disconnect(_vfs, nullptr, this, nullptr);
    disconnect(_engine, nullptr, _vfs, nullptr);

    Vfs *oldVfs = _vfs;
    _vfs = newVfs;
    oldVfs->deleteLater();

    // Restart VFS.
    _definition.setVirtualFilesMode(newMode);

    // note this is "on top of" started slot handling defined in startVfs
    if (newMode != Vfs::Off) {
        // schedule blacklisted folders for rediscovery
        connect(_vfs, &Vfs::started, this, [oldBlacklist, this] {
            for (const auto &entry : oldBlacklist) {
                journalDb()->schedulePathForRemoteDiscovery(entry);
                // Refactoring todo: from what I can see, in 98% of cases the return val of setPinState is ignored
                // do we actually need that return value?! if so why aren't we using it?
                std::ignore = vfs().setPinState(entry, PinState::OnlineOnly);
            }
        });
    }
    if (!wasPaused) {
        setSyncPaused(wasPaused);
    }
    startVfs();

    Q_EMIT vfsModeChanged(this, newMode);
}

bool Folder::isDeployed() const
{
    return _definition.isDeployed();
}

bool Folder::isFileExcludedAbsolute(const QString &fullPath) const
{
    if (OC_ENSURE(_engine)) {
        return _engine->isExcluded(fullPath);
    }
    return true;
}

bool Folder::isFileExcludedRelative(const QString &relativePath) const
{
    return isFileExcludedAbsolute(path() + relativePath);
}

void Folder::slotTerminateSync(const QString &reason)
{
    if (isReady()) {
        qCInfo(lcFolder) << "folder " << path() << " Terminating!";
        if (_engine->isSyncRunning()) {
            _engine->abort(reason);
            setSyncState(SyncResult::SyncAbortRequested);
        }
    }
}

void Folder::wipeForRemoval()
{
    // note we don't have to abort any running sync here as the folderman pauses the folder before this function is ever called.
    Q_ASSERT(!isSyncRunning());

    // I don't understand this logic so I'm removing it for now
    // the setupError condition is related to failure to start vfs as far as I can tell.
    // that doesn't mean the members don't exist, to the contrary! they are likely instantiated so let's
    // let the wipe proceed.
    // we can't acces those variables
    // if (hasSetupError()) {
    //   return;
    // }

    // prevent interaction with the db etc
    _vfsIsReady = false;

    _folderWatcher->disconnect();

    // Delete files that have been partially downloaded.
    slotDiscardDownloadProgress();

    // Unregister the socket API so it does not keep the .sync_journal file open
    FolderMan::instance()->socketApi()->slotUnregisterPath(this);
    _journal->close(); // close the sync journal

    // Remove db and temporaries
    // what is this? the engine's journal IS THE SAME as the folder member journal!!!!
    // const QString stateDbFile = _engine->journal()->databaseFilePath();
    const QString stateDbFile = _journal->databaseFilePath();


    QFile file(stateDbFile);
    if (file.exists()) {
        if (!file.remove()) {
            qCCritical(lcFolder) << "Failed to remove existing csync StateDB " << stateDbFile;
        } else {
            qCInfo(lcFolder) << "wipe: Removed csync StateDB " << stateDbFile;
        }
    } else {
        qCWarning(lcFolder) << "statedb is empty, can not remove.";
    }

    // Also remove other db related files
    QFile::remove(stateDbFile + QStringLiteral(".ctmp"));
    QFile::remove(stateDbFile + QStringLiteral("-shm"));
    QFile::remove(stateDbFile + QStringLiteral("-wal"));
    QFile::remove(stateDbFile + QStringLiteral("-journal"));

    _vfs->stop();
    _vfs->unregisterFolder();

    // don't kill vfs pointer, as it is a child of the Folder we should let it be auto-deleted by normal qobject destruction sequence.
}

bool Folder::reloadExcludes()
{
    // reloadExcludes returns true *if the excludes were successfully reloaded*.
    // If the engine is missing the excludes
    // can't be reloaded so return here should be false? Erik agrees - caller never checks the value anyway
    if (!_engine) {
        return false;
    }
    return _engine->reloadExcludes();
}

void Folder::startSync()
{
    if (!isReady() || !_folderWatcher) {
        qCWarning(lcFolder) << "Folder sync attempted before ready and/or without valid folder watcher";
        return;
    }

    if (!OC_ENSURE(!isSyncRunning())) {
        qCCritical(lcFolder) << "ERROR sync is still running and new sync requested.";
        return;
    }

    if (!OC_ENSURE(canSync())) {
        qCCritical(lcFolder) << "ERROR folder is currently not sync able.";
        return;
    }

    _timeSinceLastSyncStart.start();
    setSyncState(SyncResult::SyncPrepare);
    _syncResult.reset();

    qCInfo(lcFolder) << "*** Start syncing " << remoteUrl().toString() << "client version"
                     << Theme::instance()->aboutVersions(Theme::VersionFormat::OneLiner);

    _fileLog->start(path());

    if (!reloadExcludes()) {
        slotSyncError(tr("Could not read system exclude file"));
        QMetaObject::invokeMethod(
            this, [this] { slotSyncFinished(false); }, Qt::QueuedConnection);
        return;
    }

    // get the latest touched files
    // this will enqueue this folder again, it doesn't matter
    slotWatchedPathsChanged(_folderWatcher->popChangeSet(), Folder::ChangeReason::Other);

    const std::chrono::milliseconds fullLocalDiscoveryInterval = ConfigFile().fullLocalDiscoveryInterval();
    const bool hasDoneFullLocalDiscovery = _timeSinceLastFullLocalDiscovery.isValid();
    // negative fullLocalDiscoveryInterval means we don't require periodic full runs
    const bool periodicFullLocalDiscoveryNow =
        fullLocalDiscoveryInterval.count() >= 0 && _timeSinceLastFullLocalDiscovery.hasExpired(fullLocalDiscoveryInterval.count());
    if (_folderWatcher && _folderWatcher->isReliable()
        && hasDoneFullLocalDiscovery
        && !periodicFullLocalDiscoveryNow) {
        qCInfo(lcFolder) << "Allowing local discovery to read from the database";
        _engine->setLocalDiscoveryOptions(
            LocalDiscoveryStyle::DatabaseAndFilesystem,
            _localDiscoveryTracker->localDiscoveryPaths());
        _localDiscoveryTracker->startSyncPartialDiscovery();
    } else {
        qCInfo(lcFolder) << "Forbidding local discovery to read from the database";
        _engine->setLocalDiscoveryOptions(LocalDiscoveryStyle::FilesystemOnly);
        _localDiscoveryTracker->startSyncFullDiscovery();
    }

    QMetaObject::invokeMethod(_engine, &SyncEngine::startSync, Qt::QueuedConnection);

    Q_EMIT syncStarted();
}

void Folder::setMoveToTrash(bool trashIt)
{
    _engine->setMoveToTrash(trashIt);
}

void Folder::slotSyncError(const QString &message, ErrorCategory category)
{
    _syncResult.appendErrorString(message);
    Q_EMIT ProgressDispatcher::instance()->syncError(this, message, category);
}

void Folder::slotSyncStarted()
{
    qCInfo(lcFolder) << "#### Propagation start ####################################################";
    setSyncState(SyncResult::SyncRunning);
}

void Folder::slotSyncFinished(bool success)
{
    if (!isReady()) {
        // probably removing the folder
        qCWarning(lcFolder) << "Folder not ready after sync finished";
        return;
    }
    qCInfo(lcFolder) << "Client version" << Theme::instance()->aboutVersions(Theme::VersionFormat::OneLiner);

    bool syncError = !_syncResult.errorStrings().isEmpty();
    if (syncError) {
        qCWarning(lcFolder) << "SyncEngine finished with ERROR";
    } else {
        qCInfo(lcFolder) << "SyncEngine finished without problem.";
    }
    _fileLog->finish();
    showSyncResultPopup();

    auto anotherSyncNeeded = false;

    auto syncStatus = SyncResult::Status::Undefined;

    if (syncError) {
        syncStatus = SyncResult::Error;
    } else if (_syncResult.foundFilesNotSynced()) {
        syncStatus = SyncResult::Problem;
    } else if (_definition.paused()) {
        // Maybe the sync was terminated because the user paused the folder
        syncStatus = SyncResult::Paused;
    } else {
        syncStatus = SyncResult::Success;
    }

    // Count the number of syncs that have failed in a row.
    if (syncStatus == SyncResult::Success || syncStatus == SyncResult::Problem) {
        _consecutiveFailingSyncs = 0;
    } else {
        _consecutiveFailingSyncs++;
        anotherSyncNeeded |= _consecutiveFailingSyncs <= retrySyncLimitC;
        qCInfo(lcFolder) << "the last" << _consecutiveFailingSyncs << "syncs failed";
    }

    if (syncStatus == SyncResult::Success && success) {
        // Clear the white list as all the folders that should be on that list are sync-ed
        journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, {});
    }

    if ((syncStatus == SyncResult::Success || syncStatus == SyncResult::Problem) && success) {
        if (_engine->lastLocalDiscoveryStyle() == LocalDiscoveryStyle::FilesystemOnly) {
            _timeSinceLastFullLocalDiscovery.start();
        }
    }

    if (syncStatus != SyncResult::Undefined) {
        setSyncState(syncStatus);
    }

    // syncStateChange from setSyncState needs to be emitted first
    QTimer::singleShot(0, this, [this] { Q_EMIT syncFinished(_syncResult); });

    _lastSyncDuration = std::chrono::milliseconds(_timeSinceLastSyncStart.elapsed());
    _timeSinceLastSyncDone.start();

    // Increment the follow-up sync counter if necessary.
    if (_engine->isAnotherSyncNeeded()) {
        _consecutiveFollowUpSyncs++;
        anotherSyncNeeded |= _consecutiveFollowUpSyncs <= retrySyncLimitC;
        qCInfo(lcFolder) << "another sync was requested by the finished sync, this has"
                         << "happened" << _consecutiveFollowUpSyncs << "times";
    } else {
        _consecutiveFollowUpSyncs = 0;
    }

    // Maybe force a follow-up sync to take place, but only a couple of times.
    if (anotherSyncNeeded && canSync()) {
        // Sometimes another sync is requested because a local file is still
        // changing, so wait at least a small amount of time before syncing
        // the folder again.
        QTimer::singleShot(SyncEngine::minimumFileAgeForUpload, this, [this] { FolderMan::instance()->scheduler()->enqueueFolder(this); });
    }
}

// an item is completed: count the errors and forward to the ProgressDispatcher
void Folder::slotItemCompleted(const SyncFileItemPtr &item)
{
    if (item->_status == SyncFileItem::Success && (item->instruction() & (CSYNC_INSTRUCTION_NONE | CSYNC_INSTRUCTION_UPDATE_METADATA))) {
        // We only care about the updates that deserve to be shown in the UI
        return;
    }

    _syncResult.processCompletedItem(item);

    _fileLog->logItem(*item);
    Q_EMIT ProgressDispatcher::instance()->itemCompleted(this, item);
}

void Folder::slotLogPropagationStart()
{
    _fileLog->logLap(QStringLiteral("Propagation starts"));
}

void Folder::slotNextSyncFullLocalDiscovery()
{
    _timeSinceLastFullLocalDiscovery.invalidate();
}

void Folder::schedulePathForLocalDiscovery(const QString &relativePath)
{
    _localDiscoveryTracker->addTouchedPath(relativePath);
}

void Folder::slotFolderConflicts(Folder *folder, const QStringList &conflictPaths)
{
    if (folder != this)
        return;
    auto &r = _syncResult;

    // If the number of conflicts is too low, adjust it upwards
    if (conflictPaths.size() > r.numNewConflictItems() + r.numOldConflictItems())
        r.setNumOldConflictItems(conflictPaths.size() - r.numNewConflictItems());
}

void Folder::warnOnNewExcludedItem(const SyncJournalFileRecord &record, QStringView path)
{
    // Never warn for items in the database
    if (record.isValid())
        return;

    // Don't warn for items that no longer exist.
    // Note: This assumes we're getting file watcher notifications
    // for folders only on creation and deletion - if we got a notification
    // on content change that would create spurious warnings.
    QFileInfo fi(_definition.canonicalPath() + path);
    if (!fi.exists())
        return;

    bool ok = false;
    auto blacklist = _journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, ok);
    if (!ok)
        return;
    if (!blacklist.contains(path + QLatin1Char('/')))
        return;

    const auto message = fi.isDir()
        ? tr("The folder %1 was created but was excluded from synchronization previously. "
             "Data inside it will not be synchronized.")
              .arg(fi.filePath())
        : tr("The file %1 was created but was excluded from synchronization previously. "
             "It will not be synchronized.")
              .arg(fi.filePath());

    ocApp()->gui()->slotShowOptionalTrayMessage(Theme::instance()->appNameGUI(), message);
}

void Folder::slotWatcherUnreliable(const QString &message)
{
    qCWarning(lcFolder) << "Folder watcher for" << path() << "became unreliable:" << message;

    QMessageBox *msgBox = new QMessageBox(QMessageBox::Information, Theme::instance()->appNameGUI(),
        tr("Changes in synchronized folders could not be tracked reliably.\n"
           "\n"
           "This means that the synchronization client might not upload local changes "
           "immediately and will instead only scan for local changes and upload them "
           "occasionally (every two hours by default).\n"
           "\n"
           "%1")
            .arg(message),
        {}, ocApp()->gui()->settingsDialog());

    msgBox->setAttribute(Qt::WA_DeleteOnClose);
    ownCloudGui::raise();
    msgBox->open();
}

void Folder::registerFolderWatcher()
{
    if (_folderWatcher) {
        return;
    }

    _folderWatcher = new FolderWatcher(this);
    connect(
        _folderWatcher, &FolderWatcher::pathChanged, this, [this](const QSet<QString> &paths) { slotWatchedPathsChanged(paths, Folder::ChangeReason::Other); });
    connect(_folderWatcher, &FolderWatcher::lostChanges, this, &Folder::slotNextSyncFullLocalDiscovery);
    connect(_folderWatcher, &FolderWatcher::becameUnreliable, this, &Folder::slotWatcherUnreliable);
    _folderWatcher->init(path());
    _folderWatcher->startNotificatonTest(path() + QLatin1String(".owncloudsync.log"));
}

bool Folder::virtualFilesEnabled() const
{
    return _definition.virtualFilesMode() != Vfs::Off;
}

FolderDefinition::FolderDefinition(const QByteArray &id, const QUrl &davUrl, const QString &spaceId, const QString &displayName)
    : _webDavUrl(davUrl)
    , _spaceId(spaceId)
    , _id(id)
    , _displayName(displayName)
{
    if (_webDavUrl.path().endsWith(QLatin1Char('/'))) {
        // this is related to folder wizard adding trailing separator. No other impl adds trailing sep to the webDavUrl
        // so we want to clean these corner cases up so the paths are consistent. the folder wizard has been updated to
        // no longer add the trailing sep so this just cleans  up "legacy" paths from previously existing configs
        QString path = _webDavUrl.path();
        path.truncate(path.lastIndexOf('/'));
        _webDavUrl.setPath(path);

        // note if we want to add a trailing slash to the webDavUrl in future, it should be done in Space::webDavUrl since
        // that is the ultimate source of the url. we should not be "fixing" separators all over the app.
    }
}

FolderDefinition::FolderDefinition(const QUrl &davUrl, const QString &spaceId, const QString &displayName)
    : FolderDefinition(QUuid::createUuid().toByteArray(QUuid::WithoutBraces), davUrl, spaceId, displayName)
{
}

void FolderDefinition::setPriority(uint32_t newPriority)
{
    _priority = newPriority;
}

uint32_t FolderDefinition::priority() const
{
    return _priority;
}

void FolderDefinition::save(QSettings &settings, const FolderDefinition &def)
{
    settings.setValue("localPath", def.localPath());
    settings.setValue("journalPath", def.journalPath());
    settings.setValue("targetPath", def.targetPath());
    if (!def.spaceId().isEmpty()) {
        settings.setValue(spaceIdC(), def.spaceId());
    }
    settings.setValue(davUrlC(), def.webDavUrl());
    settings.setValue(displayNameC(), def.displayName());
    settings.setValue("paused", def.paused());
    settings.setValue(deployedC(), def.isDeployed());
    settings.setValue(priorityC(), def.priority());

    settings.setValue("virtualFilesMode", Utility::enumToString(def.virtualFilesMode()));
}

FolderDefinition FolderDefinition::load(QSettings &settings, const QByteArray &id)
{
    Q_ASSERT(!id.isEmpty());

    FolderDefinition def{id, settings.value(davUrlC()).toUrl(), settings.value(spaceIdC()).toString(), settings.value(displayNameC()).toString()};
    def.setLocalPath(settings.value("localPath").toString());
    def.setTargetPath(settings.value("targetPath").toString());
    def._journalPath = settings.value("journalPath").toString();
    def._paused = settings.value("paused").toBool();
    def._deployed = settings.value(deployedC(), false).toBool();
    def._priority = settings.value(priorityC(), 0).toUInt();

    def._virtualFilesMode = Vfs::Off;
    QString vfsModeString = settings.value(QStringLiteral("virtualFilesMode")).toString();
    if (!vfsModeString.isEmpty()) {
        def._virtualFilesMode = Vfs::modeFromString(vfsModeString);
    }

    if (settings.contains("version")) {
        // Migration from pre-7.0:
        settings.remove("version");
    }

    const QVariant ignoreHiddenFiles = settings.value("ignoreHiddenFiles");
    if (!ignoreHiddenFiles.isNull()) {
        // Migration from pre-7.0:
        settings.remove("ignoreHiddenFiles");
        FolderMan::instance()->setIgnoreHiddenFiles(ignoreHiddenFiles.toBool());
    }

    return def;
}

void FolderDefinition::setLocalPath(const QString &path)
{
    _localPath = QDir::fromNativeSeparators(path);
    if (!_localPath.endsWith(QLatin1Char('/'))) {
        _localPath.append(QLatin1Char('/'));
    }

    QFileInfo fi(_localPath);
    _canonicalLocalPath = fi.canonicalFilePath();

    if (_canonicalLocalPath.isEmpty()) {
        qCWarning(lcFolder) << "Broken symlink:" << _localPath;
        _canonicalLocalPath = _localPath;
    } else if (!_canonicalLocalPath.endsWith(QLatin1Char('/'))) {
        // the canonicalPath function strips off the trailing separator so we have to add it back, apparently
        _canonicalLocalPath.append(QLatin1Char('/'));
    }
}

void FolderDefinition::setTargetPath(const QString &path)
{
    _targetPath = Utility::stripTrailingSlash(path);
    // Doing this second ensures the empty string or "/" come
    // out as "/".
    if (!_targetPath.startsWith(QLatin1Char('/'))) {
        _targetPath.prepend(QLatin1Char('/'));
    }
}

QString FolderDefinition::displayName() const
{
    if (_displayName.isEmpty()) {
        if (targetPath().length() > 0 && targetPath() != QLatin1String("/")) {
            QString a = QFileInfo(targetPath()).fileName();
            if (a.startsWith(QLatin1Char('/'))) {
                a = a.remove(0, 1);
            }
            return a;
        } else {
            return Theme::instance()->appNameGUI();
        }
    }
    return _displayName;
}

} // namespace OCC
