/*
 * 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 "folderman.h"
#include "configfile.h"
#include "folder.h"
#include "syncresult.h"
#include "theme.h"
#include "socketapi.h"
#include "account.h"
#include "accountstate.h"
#include "accountmanager.h"
#include "filesystem.h"
#include "lockwatcher.h"
#include "asserts.h"
#include <syncengine.h>

#ifdef Q_OS_MAC
#include <CoreServices/CoreServices.h>
#endif
#ifdef Q_OS_WIN
#include <shlobj.h>
#endif

#include <QMessageBox>
#include <QtCore>
#include <QMutableSetIterator>
#include <QSet>

namespace OCC {

FolderMan* FolderMan::_instance = 0;

FolderMan::FolderMan(QObject *parent) :
    QObject(parent),
    _currentSyncFolder(0),
    _syncEnabled( true ),
    _lockWatcher(new LockWatcher),
    _appRestartRequired(false)
{
    ASSERT(!_instance);
    _instance = this;

    _socketApi.reset(new SocketApi);

    ConfigFile cfg;
    int polltime = cfg.remotePollInterval();
    qDebug() << "setting remote poll timer interval to" << polltime << "msec";
    _etagPollTimer.setInterval( polltime );
    QObject::connect(&_etagPollTimer, SIGNAL(timeout()), this, SLOT(slotEtagPollTimerTimeout()));
    _etagPollTimer.start();

    _startScheduledSyncTimer.setSingleShot(true);
    connect(&_startScheduledSyncTimer, SIGNAL(timeout()),
            SLOT(slotStartScheduledFolderSync()));

    _timeScheduler.setInterval(5000);
    _timeScheduler.setSingleShot(false);
    connect(&_timeScheduler, SIGNAL(timeout()),
            SLOT(slotScheduleFolderByTime()));
    _timeScheduler.start();

    connect(AccountManager::instance(), SIGNAL(accountRemoved(AccountState*)),
            SLOT(slotRemoveFoldersForAccount(AccountState*)));

    connect(_lockWatcher.data(), SIGNAL(fileUnlocked(QString)),
            SLOT(slotWatchedFileUnlocked(QString)));
}

FolderMan *FolderMan::instance()
{
    return _instance;
}

FolderMan::~FolderMan()
{
    qDeleteAll(_folderMap);
    _instance = 0;
}

OCC::Folder::Map FolderMan::map()
{
    return _folderMap;
}

void FolderMan::unloadFolder( Folder *f )
{
    if( !f ) {
        return;
    }

    _socketApi->slotUnregisterPath(f->alias());

    if( _folderWatchers.contains(f->alias())) {
        _folderWatchers.remove(f->alias());
    }
    _folderMap.remove( f->alias() );

    disconnect(f, SIGNAL(syncStarted()),
               this, SLOT(slotFolderSyncStarted()));
    disconnect(f, SIGNAL(syncFinished(SyncResult)),
               this, SLOT(slotFolderSyncFinished(SyncResult)));
    disconnect(f, SIGNAL(syncStateChange()),
               this, SLOT(slotForwardFolderSyncStateChange()));
    disconnect(f, SIGNAL(syncPausedChanged(Folder*,bool)),
               this, SLOT(slotFolderSyncPaused(Folder*,bool)));
    disconnect(&f->syncEngine().syncFileStatusTracker(), SIGNAL(fileStatusChanged(const QString &, SyncFileStatus)),
               _socketApi.data(), SLOT(broadcastStatusPushMessage(const QString &, SyncFileStatus)));
    disconnect(f, SIGNAL(watchedFileChangedExternally(QString)),
               &f->syncEngine().syncFileStatusTracker(), SLOT(slotPathTouched(QString)));
}

int FolderMan::unloadAndDeleteAllFolders()
{
    int cnt = 0;

    // clear the list of existing folders.
    Folder::MapIterator i(_folderMap);
    while (i.hasNext()) {
        i.next();
        Folder* f = i.value();
        unloadFolder(f);
        delete f;
        cnt++;
    }
    ASSERT(_folderMap.isEmpty());

    _lastSyncFolder = 0;
    _currentSyncFolder = 0;
    _scheduledFolders.clear();
    emit scheduleQueueChanged();

    return cnt;
}

// add a monitor to the local file system. If there is a change in the
// file system, the method slotFolderMonitorFired is triggered through
// the SignalMapper
void FolderMan::registerFolderMonitor( Folder *folder )
{
    if( !folder ) return;
    if( !QDir(folder->path()).exists() ) return;

    if( !_folderWatchers.contains(folder->alias() ) ) {
        FolderWatcher *fw = new FolderWatcher(folder->path(), folder);

        // Connect the pathChanged signal, which comes with the changed path,
        // to the signal mapper which maps to the folder alias. The changed path
        // is lost this way, but we do not need it for the current implementation.
        connect(fw, SIGNAL(pathChanged(QString)), folder, SLOT(slotWatchedPathChanged(QString)));

        _folderWatchers.insert(folder->alias(), fw);
    }

    // register the folder with the socket API
    if (folder->canSync())
        _socketApi->slotRegisterPath(folder->alias());
}

void FolderMan::addMonitorPath( const QString& alias, const QString& path )
{
    if( !alias.isEmpty() && _folderWatchers.contains(alias) ) {
        FolderWatcher *fw = _folderWatchers[alias];

        if( fw ) {
            fw->addPath(path);
        }
    }
}

void FolderMan::removeMonitorPath( const QString& alias, const QString& path )
{
    if( !alias.isEmpty() && _folderWatchers.contains(alias) ) {
        FolderWatcher *fw = _folderWatchers[alias];

        if( fw ) {
            fw->removePath(path);
        }
    }
}

int FolderMan::setupFolders()
{
    unloadAndDeleteAllFolders();

    auto settings = Utility::settingsWithGroup(QLatin1String("Accounts"));
    const auto accountsWithSettings = settings->childGroups();
    if (accountsWithSettings.isEmpty()) {
        int r = setupFoldersMigration();
        if (r > 0) {
            AccountManager::instance()->save(false); // don't save credentials, they had not been loaded from keychain
        }
        return r;
    }

    qDebug() << "* Setup folders from settings file";

    foreach (const auto& account, AccountManager::instance()->accounts()) {
        const auto id = account->account()->id();
        if (!accountsWithSettings.contains(id)) {
            continue;
        }
        settings->beginGroup(id);

        settings->beginGroup(QLatin1String("Folders"));
        setupFoldersHelper(*settings, account, true);
        settings->endGroup();

        // See Folder::saveToSettings for details about why this exists.
        settings->beginGroup(QLatin1String("Multifolders"));
        setupFoldersHelper(*settings, account, false);
        settings->endGroup();

        settings->endGroup(); // <account>
    }

    emit folderListChanged(_folderMap);

    return _folderMap.size();
}

void FolderMan::setupFoldersHelper(QSettings &settings, AccountStatePtr account, bool backwardsCompatible)
{
    foreach (const auto& folderAlias, settings.childGroups()) {
        FolderDefinition folderDefinition;
        if (FolderDefinition::load(settings, folderAlias, &folderDefinition)) {
            // Migration: Old settings don't have journalPath
            if (folderDefinition.journalPath.isEmpty()) {
                folderDefinition.journalPath = folderDefinition.defaultJournalPath(account->account());
            }
            folderDefinition.defaultJournalPath(account->account());
            // Migration: If an old db is found, move it to the new name.
            if (backwardsCompatible) {
                SyncJournalDb::maybeMigrateDb(folderDefinition.localPath, folderDefinition.absoluteJournalPath());
            }

            Folder* f = addFolderInternal(std::move(folderDefinition), account.data());
            if (f) {
                // Migration: Mark folders that shall be saved in a backwards-compatible way
                if (backwardsCompatible) {
                    f->setSaveBackwardsCompatible(true);
                }
                scheduleFolder(f);
                emit folderSyncStateChange(f);
            }
        }
    }
}

int FolderMan::setupFoldersMigration()
{
    ConfigFile cfg;
    QDir storageDir(cfg.configPath());
    _folderConfigPath = cfg.configPath() + QLatin1String("folders");

    qDebug() << "* Setup folders from " << _folderConfigPath << "(migration)";

    QDir dir( _folderConfigPath );
    //We need to include hidden files just in case the alias starts with '.'
    dir.setFilter(QDir::Files | QDir::Hidden);
    QStringList list = dir.entryList();

    // Normally there should be only one account when migrating.
    AccountState* accountState = AccountManager::instance()->accounts().value(0).data();
    foreach ( const QString& alias, list ) {
        Folder *f = setupFolderFromOldConfigFile( alias, accountState );
        if( f ) {
            scheduleFolder(f);
            emit folderSyncStateChange(f);
        }
    }

    emit folderListChanged(_folderMap);

    // return the number of valid folders.
    return _folderMap.size();
}

bool FolderMan::ensureJournalGone( const QString& journalDbFile )
{
    // remove the old journal file
    while (QFile::exists(journalDbFile) && !QFile::remove(journalDbFile)) {
        qDebug() << "Could not remove old db file at" << journalDbFile;
        int ret = QMessageBox::warning(0, tr("Could not reset folder state"),
                                       tr("An old sync journal '%1' was found, "
                                          "but could not be removed. Please make sure "
                                          "that no application is currently using it.")
                                       .arg(QDir::fromNativeSeparators(QDir::cleanPath(journalDbFile))),
                                       QMessageBox::Retry|QMessageBox::Abort);
        if (ret == QMessageBox::Abort) {
            return false;
        }
    }
    return true;
}

#define SLASH_TAG   QLatin1String("__SLASH__")
#define BSLASH_TAG  QLatin1String("__BSLASH__")
#define QMARK_TAG   QLatin1String("__QMARK__")
#define PERCENT_TAG QLatin1String("__PERCENT__")
#define STAR_TAG    QLatin1String("__STAR__")
#define COLON_TAG   QLatin1String("__COLON__")
#define PIPE_TAG    QLatin1String("__PIPE__")
#define QUOTE_TAG   QLatin1String("__QUOTE__")
#define LT_TAG      QLatin1String("__LESS_THAN__")
#define GT_TAG      QLatin1String("__GREATER_THAN__")
#define PAR_O_TAG   QLatin1String("__PAR_OPEN__")
#define PAR_C_TAG   QLatin1String("__PAR_CLOSE__")

QString FolderMan::escapeAlias( const QString& alias )
{
    QString a(alias);

    a.replace( QLatin1Char('/'), SLASH_TAG );
    a.replace( QLatin1Char('\\'), BSLASH_TAG );
    a.replace( QLatin1Char('?'), QMARK_TAG  );
    a.replace( QLatin1Char('%'), PERCENT_TAG );
    a.replace( QLatin1Char('*'), STAR_TAG );
    a.replace( QLatin1Char(':'), COLON_TAG );
    a.replace( QLatin1Char('|'), PIPE_TAG );
    a.replace( QLatin1Char('"'), QUOTE_TAG );
    a.replace( QLatin1Char('<'), LT_TAG );
    a.replace( QLatin1Char('>'), GT_TAG );
    a.replace( QLatin1Char('['), PAR_O_TAG );
    a.replace( QLatin1Char(']'), PAR_C_TAG );
    return a;
}

SocketApi *FolderMan::socketApi()
{
    return this->_socketApi.data();
}

QString FolderMan::unescapeAlias( const QString& alias )
{
    QString a(alias);

    a.replace( SLASH_TAG,   QLatin1String("/") );
    a.replace( BSLASH_TAG,  QLatin1String("\\") );
    a.replace( QMARK_TAG,   QLatin1String("?")  );
    a.replace( PERCENT_TAG, QLatin1String("%") );
    a.replace( STAR_TAG,    QLatin1String("*") );
    a.replace( COLON_TAG,   QLatin1String(":") );
    a.replace( PIPE_TAG,    QLatin1String("|") );
    a.replace( QUOTE_TAG,   QLatin1String("\"") );
    a.replace( LT_TAG,      QLatin1String("<") );
    a.replace( GT_TAG,      QLatin1String(">") );
    a.replace( PAR_O_TAG,   QLatin1String("[") );
    a.replace( PAR_C_TAG,   QLatin1String("]") );

    return a;
}

// filename is the name of the file only, it does not include
// the configuration directory path
// WARNING: Do not remove this code, it is used for predefined/automated deployments (2016)
Folder* FolderMan::setupFolderFromOldConfigFile(const QString &file, AccountState *accountState )
{
    Folder *folder = 0;

    qDebug() << "  ` -> setting up:" << file;
    QString escapedAlias(file);
    // check the unescaped variant (for the case when the filename comes out
    // of the directory listing). If the file does not exist, escape the
    // file and try again.
    QFileInfo cfgFile( _folderConfigPath, file);

    if( !cfgFile.exists() ) {
        // try the escaped variant.
        escapedAlias = escapeAlias(file);
        cfgFile.setFile( _folderConfigPath, escapedAlias );
    }
    if( !cfgFile.isReadable() ) {
        qDebug() << "Cannot read folder definition for alias " << cfgFile.filePath();
        return folder;
    }

    QSettings settings( _folderConfigPath + QLatin1Char('/') + escapedAlias, QSettings::IniFormat);
    qDebug() << "    -> file path: " << settings.fileName();

    // Check if the filename is equal to the group setting. If not, use the group
    // name as an alias.
    QStringList groups = settings.childGroups();

    if( ! groups.contains(escapedAlias) && groups.count() > 0 ) {
        escapedAlias = groups.first();
    }

    settings.beginGroup( escapedAlias ); // read the group with the same name as the file which is the folder alias

    QString path = settings.value(QLatin1String("localPath")).toString();
    QString backend = settings.value(QLatin1String("backend")).toString();
    QString targetPath = settings.value( QLatin1String("targetPath")).toString();
    bool paused = settings.value( QLatin1String("paused"), false).toBool();
    // QString connection = settings.value( QLatin1String("connection") ).toString();
    QString alias = unescapeAlias( escapedAlias );

    if (backend.isEmpty() || backend != QLatin1String("owncloud")) {
        qWarning() << "obsolete configuration of type" << backend;
        return 0;
    }

    // cut off the leading slash, oCUrl always has a trailing.
    if( targetPath.startsWith(QLatin1Char('/')) ) {
        targetPath.remove(0,1);
    }

    if (!accountState) {
        qWarning() << "can't create folder without an account";
        return 0;
    }

    FolderDefinition folderDefinition;
    folderDefinition.alias = alias;
    folderDefinition.localPath = path;
    folderDefinition.targetPath = targetPath;
    folderDefinition.paused = paused;
    folderDefinition.ignoreHiddenFiles = ignoreHiddenFiles();

    folder = addFolderInternal(folderDefinition, accountState);
    if (folder) {
        QStringList blackList = settings.value( QLatin1String("blackList")).toStringList();
        if (!blackList.empty()) {
            //migrate settings
            folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList);
            settings.remove(QLatin1String("blackList"));
            // FIXME: If you remove this codepath, you need to provide another way to do
            // this via theme.h or the normal FolderMan::setupFolders
        }

        folder->saveToSettings();
    }
    qDebug() << "Migrated!" << folder;
    settings.sync();
    return folder;
}

void FolderMan::slotFolderSyncPaused( Folder *f, bool paused )
{
    if( !f ) {
        qWarning() << "!! slotFolderSyncPaused called with empty folder";
        return;
    }

    if (!paused) {
        _disabledFolders.remove(f);
        scheduleFolder(f);
    } else {
        _disabledFolders.insert(f);
    }
}

void FolderMan::slotFolderCanSyncChanged()
{
    Folder *f = qobject_cast<Folder*>(sender());
    ASSERT(f);
    if (f->canSync()) {
        _socketApi->slotRegisterPath(f->alias());
    } else {
        _socketApi->slotUnregisterPath(f->alias());
    }
}

// this really terminates the current sync process
// ie. no questions, no prisoners
// csync still remains in a stable state, regardless of that.
void FolderMan::terminateSyncProcess()
{
    Folder *f = _currentSyncFolder;
    if( f ) {
        // This will, indirectly and eventually, call slotFolderSyncFinished
        // and thereby clear _currentSyncFolder.
        f->slotTerminateSync();
    }
}

Folder *FolderMan::folder( const QString& alias )
{
    if( !alias.isEmpty() ) {
        if( _folderMap.contains( alias )) {
            return _folderMap[alias];
        }
    }
    return 0;
}

void FolderMan::scheduleAllFolders()
{
    foreach( Folder *f, _folderMap.values() ) {
        if (f && f->canSync()) {
            scheduleFolder( f );
        }
    }
}

void FolderMan::slotScheduleAppRestart()
{
    _appRestartRequired = true;
    qDebug() << "## Application restart requested!";
}

void FolderMan::slotSyncOnceFileUnlocks(const QString& path)
{
    _lockWatcher->addFile(path);
}

/*
  * if a folder wants to be synced, it calls this slot and is added
  * to the queue. The slot to actually start a sync is called afterwards.
  */
void FolderMan::scheduleFolder( Folder *f )
{
    if( !f ) {
        qWarning() << "slotScheduleSync called with null folder";
        return;
    }
    auto alias = f->alias();

    qDebug() << "Schedule folder " << alias << " to sync!";

    if( ! _scheduledFolders.contains(f) ) {
        if( !f->canSync() ) {
            qDebug() << "Folder is not ready to sync, not scheduled!";
            _socketApi->slotUpdateFolderView(f);
            return;
        }
        f->prepareToSync();
        emit folderSyncStateChange(f);
        _scheduledFolders.enqueue(f);
        emit scheduleQueueChanged();
    } else {
        qDebug() << " II> Sync for folder " << alias << " already scheduled, do not enqueue!";
    }

    startScheduledSyncSoon();
}

void FolderMan::scheduleFolderNext(Folder* f)
{
    auto alias = f->alias();
    qDebug() << "Schedule folder " << alias << " to sync! Front-of-queue.";

    if( !f->canSync() ) {
        qDebug() << "Folder is not ready to sync, not scheduled!";
        return;
    }

    _scheduledFolders.removeAll(f);

    f->prepareToSync();
    emit folderSyncStateChange(f);
    _scheduledFolders.prepend(f);
    emit scheduleQueueChanged();

    startScheduledSyncSoon();
}

void FolderMan::slotScheduleETagJob(const QString &/*alias*/, RequestEtagJob *job)
{
    QObject::connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotEtagJobDestroyed(QObject*)));
    QMetaObject::invokeMethod(this, "slotRunOneEtagJob", Qt::QueuedConnection);
    // maybe: add to queue
}

void FolderMan::slotEtagJobDestroyed(QObject* /*o*/)
{
    // _currentEtagJob is automatically cleared
    // maybe: remove from queue
    QMetaObject::invokeMethod(this, "slotRunOneEtagJob", Qt::QueuedConnection);
}

void FolderMan::slotRunOneEtagJob()
{
    if (_currentEtagJob.isNull()) {
        Folder *folder;
        foreach(Folder *f, _folderMap) {
            if (f->etagJob()) {
                // Caveat: always grabs the first folder with a job, but we think this is Ok for now and avoids us having a seperate queue.
                _currentEtagJob = f->etagJob();
                folder = f;
                break;
            }
        }
        if (_currentEtagJob.isNull()) {
            //qDebug() << "No more remote ETag check jobs to schedule.";

            /* now it might be a good time to check for restarting... */
            if( _currentSyncFolder == NULL && _appRestartRequired ) {
                restartApplication();
            }
        } else {
            qDebug() << "Scheduling" << folder->remoteUrl().toString() << "to check remote ETag";
            _currentEtagJob->start(); // on destroy/end it will continue the queue via slotEtagJobDestroyed
        }
    }
}

void FolderMan::slotAccountStateChanged()
{
    AccountState * accountState = qobject_cast<AccountState*>(sender());
    if (! accountState) {
        return;
    }
    QString accountName = accountState->account()->displayName();

    if (accountState->isConnected()) {
        qDebug() << "Account" << accountName << "connected, scheduling its folders";

        foreach (Folder *f, _folderMap.values()) {
            if (f
                    && f->canSync()
                    && f->accountState() == accountState) {
                scheduleFolder(f);
            }
        }
    } else {
        qDebug() << "Account" << accountName << "disconnected or paused, "
                    "terminating or descheduling sync folders";

        if (_currentSyncFolder
                && _currentSyncFolder->accountState() == accountState) {
            _currentSyncFolder->slotTerminateSync();
        }

        QMutableListIterator<Folder*> it(_scheduledFolders);
        while (it.hasNext()) {
            Folder* f = it.next();
            if (f->accountState() == accountState) {
                it.remove();
            }
        }
        emit scheduleQueueChanged();
    }
}

// only enable or disable foldermans will schedule and do syncs.
// this is not the same as Pause and Resume of folders.
void FolderMan::setSyncEnabled( bool enabled )
{
    if (!_syncEnabled && enabled && !_scheduledFolders.isEmpty()) {
        // We have things in our queue that were waiting for the connection to come back on.
        startScheduledSyncSoon();
    }
    _syncEnabled = enabled;
    // force a redraw in case the network connect status changed
    emit( folderSyncStateChange(0) );
}

void FolderMan::startScheduledSyncSoon()
{
    if (_startScheduledSyncTimer.isActive()) {
        return;
    }
    if (_scheduledFolders.empty()) {
        return;
    }
    if (_currentSyncFolder) {
        return;
    }

    qint64 msDelay = 100; // 100ms minimum delay
    qint64 msSinceLastSync = 0;

    // Require a pause based on the duration of the last sync run.
    if (Folder* lastFolder = _lastSyncFolder) {
        msSinceLastSync = lastFolder->msecSinceLastSync();

        //  1s   -> 1.5s pause
        // 10s   -> 5s pause
        //  1min -> 12s pause
        //  1h   -> 90s pause
        qint64 pause = qSqrt(lastFolder->msecLastSyncDuration()) / 20.0 * 1000.0;
        msDelay = qMax(msDelay, pause);
    }

    // Delays beyond one minute seem too big, particularly since there
    // could be things later in the queue that shouldn't be punished by a
    // long delay!
    msDelay = qMin(msDelay, 60*1000ll);

    // Time since the last sync run counts against the delay
    msDelay = qMax(1ll, msDelay - msSinceLastSync);

    qDebug() << "Starting the next scheduled sync in" << (msDelay/1000) << "seconds";
    _startScheduledSyncTimer.start(msDelay);
}

/*
  * slot to start folder syncs.
  * It is either called from the slot where folders enqueue themselves for
  * syncing or after a folder sync was finished.
  */
void FolderMan::slotStartScheduledFolderSync()
{
    if( _currentSyncFolder ) {
        qDebug() << "Currently folder " << _currentSyncFolder->remoteUrl().toString() << " is running, wait for finish!";
        return;
    }

    if( ! _syncEnabled ) {
        qDebug() << "FolderMan: Syncing is disabled, no scheduling.";
        return;
    }

    qDebug() << "XX slotScheduleFolderSync: folderQueue size: " << _scheduledFolders.count();
    if( _scheduledFolders.isEmpty() ) {
        return;
    }

    // Find the first folder in the queue that can be synced.
    Folder* folder = 0;
    while( !_scheduledFolders.isEmpty() ) {
        Folder* g = _scheduledFolders.dequeue();
        if( g->canSync() ) {
            folder = g;
            break;
        }
    }

    emit scheduleQueueChanged();

    // Start syncing this folder!
    if( folder ) {
        // Safe to call several times, and necessary to try again if
        // the folder path didn't exist previously.
        registerFolderMonitor(folder);

        _currentSyncFolder = folder;
        folder->startSync( QStringList() );
    }
}

void FolderMan::slotEtagPollTimerTimeout()
{
    //qDebug() << Q_FUNC_INFO << "Checking if we need to make any folders check the remote ETag";
    ConfigFile cfg;
    int polltime = cfg.remotePollInterval();

    foreach (Folder *f, _folderMap) {
        if (!f) {
            continue;
        }
        if (_currentSyncFolder == f) {
            continue;
        }
        if (_scheduledFolders.contains(f)) {
            continue;
        }
        if (_disabledFolders.contains(f)) {
            continue;
        }
        if (f->etagJob() || f->isBusy() || !f->canSync()) {
            continue;
        }
        if (f->msecSinceLastSync() < polltime) {
            continue;
        }
        QMetaObject::invokeMethod(f, "slotRunEtagJob", Qt::QueuedConnection);
    }
}

void FolderMan::slotRemoveFoldersForAccount(AccountState* accountState)
{
    QVarLengthArray<Folder *, 16> foldersToRemove;
    Folder::MapIterator i(_folderMap);
    while (i.hasNext()) {
        i.next();
        Folder* folder = i.value();
        if (folder->accountState() == accountState) {
            foldersToRemove.append(folder);
        }
    }

    foreach (const auto &f, foldersToRemove) {
        removeFolder(f);
    }
}

void FolderMan::slotForwardFolderSyncStateChange()
{
    if (Folder* f = qobject_cast<Folder*>(sender())) {
        emit folderSyncStateChange(f);
    }
}

void FolderMan::slotServerVersionChanged(Account *account)
{
    // Pause folders if the server version is unsupported
    if (account->serverVersionUnsupported()) {
        qDebug() << "The server version is unsupported:" << account->serverVersion()
                 << "pausing all folders on the account";

        foreach (auto& f, _folderMap) {
            if (f->accountState()->account().data() == account) {
                f->setSyncPaused(true);
            }
        }
    }
}

void FolderMan::slotWatchedFileUnlocked(const QString& path)
{
    if (Folder* f = folderForPath(path)) {
        f->scheduleThisFolderSoon();
    }
}

void FolderMan::slotScheduleFolderByTime()
{
    foreach (auto& f, _folderMap) {
        // Never schedule if syncing is disabled or when we're currently
        // querying the server for etags
        if (!f->canSync() || f->etagJob()) {
            continue;
        }

        auto msecsSinceSync = f->msecSinceLastSync();

        // Possibly it's just time for a new sync run
        bool forceSyncIntervalExpired =
                quint64(msecsSinceSync) > ConfigFile().forceSyncInterval();
        if (forceSyncIntervalExpired) {
            qDebug() << "** scheduling folder" << f->alias()
                     << "because it has been" << msecsSinceSync << "ms "
                     << "since the last sync";

            scheduleFolder(f);
            continue;
        }

        // Retry a couple of times after failure; or regularly if requested
        bool syncAgain =
                (f->consecutiveFailingSyncs() > 0 && f->consecutiveFailingSyncs() < 3)
                || f->syncEngine().isAnotherSyncNeeded() == DelayedFollowUp;
        qint64 syncAgainDelay = 10 * 1000; // 10s for the first retry-after-fail
        if (f->consecutiveFailingSyncs() > 1)
            syncAgainDelay = 60 * 1000; // 60s for each further attempt
        if (syncAgain
                && msecsSinceSync > syncAgainDelay) {
            qDebug() << "** scheduling folder" << f->alias()
                     << ", the last" << f->consecutiveFailingSyncs() << "syncs failed"
                     << ", anotherSyncNeeded" << f->syncEngine().isAnotherSyncNeeded()
                     << ", last status:" << f->syncResult().statusString()
                     << ", time since last sync:" << msecsSinceSync;

            scheduleFolder(f);
            continue;
        }

        // Do we want to retry failing syncs or another-sync-needed runs more often?
    }
}

void FolderMan::slotFolderSyncStarted( )
{
    qDebug() << ">===================================== sync started for " << _currentSyncFolder->remoteUrl().toString();
}

/*
  * a folder indicates that its syncing is finished.
  * Start the next sync after the system had some milliseconds to breath.
  * This delay is particularly useful to avoid late file change notifications
  * (that we caused ourselves by syncing) from triggering another spurious sync.
  */
void FolderMan::slotFolderSyncFinished( const SyncResult& )
{
    qDebug() << "<===================================== sync finished for " << _currentSyncFolder->remoteUrl().toString();

    _lastSyncFolder = _currentSyncFolder;
    _currentSyncFolder = 0;

    startScheduledSyncSoon();
}

Folder* FolderMan::addFolder(AccountState* accountState, const FolderDefinition& folderDefinition)
{
    // Choose a db filename
    auto definition = folderDefinition;
    definition.journalPath = definition.defaultJournalPath(accountState->account());

    if (!ensureJournalGone(definition.absoluteJournalPath())) {
        return 0;
    }

    auto folder = addFolderInternal(definition, accountState);

    // Migration: The first account that's configured for a local folder shall
    // be saved in a backwards-compatible way.
    bool oneAccountOnly = true;
    foreach (Folder* other, FolderMan::instance()->map()) {
        if (other != folder && other->cleanPath() == folder->cleanPath()) {
            oneAccountOnly = false;
            break;
        }
    }
    folder->setSaveBackwardsCompatible(oneAccountOnly);

    if(folder) {
        folder->saveToSettings();
        emit folderSyncStateChange(folder);
        emit folderListChanged(_folderMap);
    }
    return folder;
}

Folder* FolderMan::addFolderInternal(FolderDefinition folderDefinition,
                                     AccountState* accountState)
{
    auto alias = folderDefinition.alias;
    int count = 0;
    while (folderDefinition.alias.isEmpty() || _folderMap.contains(folderDefinition.alias)) {
        // There is already a folder configured with this name and folder names need to be unique
        folderDefinition.alias = alias + QString::number(++count);
    }

    auto folder = new Folder(folderDefinition, accountState, this );

    qDebug() << "Adding folder to Folder Map " << folder << folder->alias();
    _folderMap[folder->alias()] = folder;
    if (folder->syncPaused()) {
        _disabledFolders.insert(folder);
    }

    // See matching disconnects in unloadFolder().
    connect(folder, SIGNAL(syncStarted()), SLOT(slotFolderSyncStarted()));
    connect(folder, SIGNAL(syncFinished(SyncResult)), SLOT(slotFolderSyncFinished(SyncResult)));
    connect(folder, SIGNAL(syncStateChange()), SLOT(slotForwardFolderSyncStateChange()));
    connect(folder, SIGNAL(syncPausedChanged(Folder*,bool)), SLOT(slotFolderSyncPaused(Folder*,bool)));
    connect(folder, SIGNAL(canSyncChanged()), SLOT(slotFolderCanSyncChanged()));
    connect(&folder->syncEngine().syncFileStatusTracker(), SIGNAL(fileStatusChanged(const QString &, SyncFileStatus)),
            _socketApi.data(), SLOT(broadcastStatusPushMessage(const QString &, SyncFileStatus)));
    connect(folder, SIGNAL(watchedFileChangedExternally(QString)),
            &folder->syncEngine().syncFileStatusTracker(), SLOT(slotPathTouched(QString)));

    registerFolderMonitor(folder);
    return folder;
}

Folder *FolderMan::folderForPath(const QString &path)
{
    QString absolutePath = QDir::cleanPath(path)+QLatin1Char('/');

    foreach(Folder* folder, this->map().values()) {
        const QString folderPath = folder->cleanPath()+QLatin1Char('/');

        if(absolutePath.startsWith(folderPath, (Utility::isWindows() || Utility::isMac())?
                    Qt::CaseInsensitive : Qt::CaseSensitive)) {
            //qDebug() << "found folder: " << folder->path() << " for " << absolutePath;
            return folder;
        }
    }

    return 0;
}

QStringList FolderMan::findFileInLocalFolders( const QString& relPath, const AccountPtr acc )
{
    QStringList re;

    foreach(Folder* folder, this->map().values()) {
        if (acc != 0 && folder->accountState()->account() != acc) {
            continue;
        }
        QString path = folder->cleanPath();
        QString remRelPath;
        // cut off the remote path from the server path.
        remRelPath = relPath.mid(folder->remotePath().length());
        path += "/";
        path += remRelPath;
        if( QFile::exists(path) ) {
            re.append( path );
        }
    }
    return re;
}

void FolderMan::removeFolder( Folder *f )
{
    if( !f ) {
        qWarning() << "!! Can not remove null folder";
        return;
    }

    qDebug() << "Removing " << f->alias();

    const bool currentlyRunning = (_currentSyncFolder == f);
    if( currentlyRunning ) {
        // abort the sync now
        terminateSyncProcess();
    }

    if (_scheduledFolders.removeAll(f) > 0) {
        emit scheduleQueueChanged();
    }

    f->wipe();
    f->setSyncPaused(true);

    // remove the folder configuration
    f->removeFromSettings();

    unloadFolder( f);
    if( currentlyRunning ) {
        // We want to schedule the next folder once this is done
        connect(f, SIGNAL(syncFinished(SyncResult)),
                SLOT(slotFolderSyncFinished(SyncResult)));
        // Let the folder delete itself when done.
        connect(f, SIGNAL(syncFinished(SyncResult)), f, SLOT(deleteLater()));
    } else {
        delete f;
    }
}

QString FolderMan::getBackupName( QString fullPathName ) const
{
    if (fullPathName.endsWith("/"))
        fullPathName.chop(1);

    if( fullPathName.isEmpty() ) return QString::null;

     QString newName = fullPathName + tr(" (backup)");
     QFileInfo fi( newName );
     int cnt = 2;
     do {
         if( fi.exists() ) {
             newName = fullPathName + tr(" (backup %1)").arg(cnt++);
             fi.setFile(newName);
         }
     } while( fi.exists() );

     return newName;
}

bool FolderMan::startFromScratch( const QString& localFolder )
{
    if( localFolder.isEmpty() ) {
        return false;
    }

    QFileInfo fi( localFolder );
    QDir parentDir( fi.dir() );
    QString folderName = fi.fileName();

    // Adjust for case where localFolder ends with a /
    if ( fi.isDir() ) {
        folderName = parentDir.dirName();
        parentDir.cdUp();
    }

    if( fi.exists() ) {
        // It exists, but is empty -> just reuse it.
        if( fi.isDir() && fi.dir().count() == 0 ) {
            qDebug() << "startFromScratch: Directory is empty!";
            return true;
        }
        // Disconnect the socket api from the database to avoid that locking of the
        // db file does not allow to move this dir.
        Folder *f = folderForPath(localFolder);
        if(f) {
            if( localFolder.startsWith(f->path()) ) {
                _socketApi->slotUnregisterPath(f->alias());
            }
            f->journalDb()->close();
            f->slotTerminateSync(); // Normally it should not be running, but viel hilft viel
        }

        // Make a backup of the folder/file.
        QString newName = getBackupName( parentDir.absoluteFilePath( folderName ) );
        QString renameError;
        if( !FileSystem::rename( fi.absoluteFilePath(), newName, &renameError ) ) {
            qDebug() << "startFromScratch: Could not rename" << fi.absoluteFilePath()
                     << "to" << newName << "error:" << renameError;
            return false;
        }
    }

    if( !parentDir.mkdir( fi.absoluteFilePath() ) ) {
        qDebug() << "startFromScratch: Could not mkdir" << fi.absoluteFilePath();
        return false;
    }

    return true;
}

void FolderMan::setDirtyProxy(bool value)
{
    foreach( Folder *f, _folderMap.values() ) {
        if(f) {
            f->setProxyDirty(value);

            if (f->accountState() && f->accountState()->account()
                    && f->accountState()->account()->networkAccessManager()) {
                // Need to do this so we do not use the old determined system proxy
                f->accountState()->account()->networkAccessManager()->setProxy(
                            QNetworkProxy(QNetworkProxy::DefaultProxy));
            }
        }
    }
}

void FolderMan::setDirtyNetworkLimits()
{
    foreach( Folder *f, _folderMap.values() ) {
        // set only in busy folders. Otherwise they read the config anyway.
        if(f && f->isBusy()) {
            f->setDirtyNetworkLimits();
        }
    }

}

SyncResult FolderMan::accountStatus(const QList<Folder*> &folders)
{
    SyncResult overallResult;

    int cnt = folders.count();

    // if one folder: show the state of the one folder.
    // if more folders:
    // if one of them has an error -> show error
    // if one is paused, but others ok, show ok
    // do not show "problem" in the tray
    //
    if( cnt == 1 ) {
        Folder *folder = folders.at(0);
        if( folder ) {
            if( folder->syncPaused() ) {
                overallResult.setStatus(SyncResult::Paused);
            } else {
                SyncResult::Status syncStatus = folder->syncResult().status();


                switch( syncStatus ) {
                case SyncResult::Undefined:
                    overallResult.setStatus(SyncResult::Error);
                    break;
                case SyncResult::NotYetStarted:
                    overallResult.setStatus( SyncResult::NotYetStarted );
                    break;
                case SyncResult::SyncPrepare:
                    overallResult.setStatus( SyncResult::SyncPrepare );
                    break;
                case SyncResult::SyncRunning:
                    overallResult.setStatus( SyncResult::SyncRunning );
                    break;
                case SyncResult::Problem: // don't show the problem icon in tray.
                case SyncResult::Success:
                    if( overallResult.status() == SyncResult::Undefined )
                        overallResult.setStatus( SyncResult::Success );
                    break;
                case SyncResult::Error:
                    overallResult.setStatus( SyncResult::Error );
                    break;
                case SyncResult::SetupError:
                    if ( overallResult.status() != SyncResult::Error )
                        overallResult.setStatus( SyncResult::SetupError );
                    break;
                case SyncResult::SyncAbortRequested:
                    overallResult.setStatus( SyncResult::SyncAbortRequested);
                    break;
                case SyncResult::Paused:
                    overallResult.setStatus( SyncResult::Paused);
                    break;
                }
            }
        }
    } else {
        int errorsSeen = 0;
        int goodSeen = 0;
        int abortOrPausedSeen = 0;
        int runSeen = 0;
        int various = 0;

        foreach ( Folder *folder, folders ) {
            if( folder->syncPaused() ) {
                abortOrPausedSeen++;
            } else {
                SyncResult folderResult = folder->syncResult();
                SyncResult::Status syncStatus = folderResult.status();

                switch( syncStatus ) {
                case SyncResult::Undefined:
                case SyncResult::NotYetStarted:
                    various++;
                    break;
                case SyncResult::SyncPrepare:
                case SyncResult::SyncRunning:
                    runSeen++;
                    break;
                case SyncResult::Problem: // don't show the problem icon in tray.
                case SyncResult::Success:
                    goodSeen++;
                    break;
                case SyncResult::Error:
                case SyncResult::SetupError:
                    errorsSeen++;
                    break;
                case SyncResult::SyncAbortRequested:
                case SyncResult::Paused:
                    abortOrPausedSeen++;
                    // no default case on purpose, check compiler warnings
                }
            }
        }
        bool set = false;
        if( errorsSeen > 0 ) {
            overallResult.setStatus(SyncResult::Error);
            set = true;
        }
        if( !set && abortOrPausedSeen > 0 && abortOrPausedSeen == cnt ) {
            // only if all folders are paused
            overallResult.setStatus(SyncResult::Paused);
            set = true;
        }
        if( !set && runSeen > 0 ) {
            overallResult.setStatus(SyncResult::SyncRunning);
            set = true;
        }
        if( !set && goodSeen > 0 ) {
            overallResult.setStatus(SyncResult::Success);
            set = true;
        }
    }

    return overallResult;
}

QString FolderMan::statusToString( SyncResult::Status syncStatus, bool paused ) const
{
    QString folderMessage;
    switch( syncStatus ) {
    case SyncResult::Undefined:
        folderMessage = tr( "Undefined State." );
        break;
    case SyncResult::NotYetStarted:
        folderMessage = tr( "Waiting to start syncing." );
        break;
    case SyncResult::SyncPrepare:
        folderMessage = tr( "Preparing for sync." );
        break;
    case SyncResult::SyncRunning:
        folderMessage = tr( "Sync is running." );
        break;
    case SyncResult::Success:
        folderMessage = tr( "Last Sync was successful." );
        break;
    case SyncResult::Error:
        break;
    case SyncResult::Problem:
        folderMessage = tr( "Last Sync was successful, but with warnings on individual files.");
        break;
    case SyncResult::SetupError:
        folderMessage = tr( "Setup Error." );
        break;
    case SyncResult::SyncAbortRequested:
        folderMessage = tr( "User Abort." );
        break;
    case SyncResult::Paused:
        folderMessage = tr("Sync is paused.");
        break;
    // no default case on purpose, check compiler warnings
    }
    if( paused ) {
        // sync is disabled.
        folderMessage = tr( "%1 (Sync is paused)" ).arg(folderMessage);
    }
    return folderMessage;
}

QString FolderMan::checkPathValidityForNewFolder(const QString& path, const QUrl &serverUrl, bool forNewDirectory)
{
    if (path.isEmpty()) {
        return tr("No valid folder selected!");
    }

    QFileInfo selFile( path );

    if (!selFile.exists()) {
        return checkPathValidityForNewFolder(selFile.dir().path(), serverUrl, true);
    }

    if( !selFile.isDir() ) {
        return tr("The selected path is not a folder!");
    }

    if ( !selFile.isWritable() ) {
        return tr("You have no permission to write to the selected folder!");
    }

    // check if the local directory isn't used yet in another ownCloud sync
    Qt::CaseSensitivity cs = Qt::CaseSensitive;
    if( Utility::fsCasePreserving() ) {
        cs = Qt::CaseInsensitive;
    }

    for (auto i = _folderMap.constBegin(); i != _folderMap.constEnd(); ++i ) {
        Folder *f = static_cast<Folder*>(i.value());
        QString folderDir = QDir( f->path() ).canonicalPath();
        if( folderDir.isEmpty() ) {
            continue;
        }
        if( ! folderDir.endsWith(QLatin1Char('/'), cs) ) folderDir.append(QLatin1Char('/'));

        const QString folderDirClean = QDir::cleanPath(folderDir)+'/';
        const QString userDirClean = QDir::cleanPath(path)+'/';

        // folderDir follows sym links, path not.
        bool differentPathes = !Utility::fileNamesEqual(QDir::cleanPath(folderDir), QDir::cleanPath(path));

        if (!forNewDirectory && differentPathes && folderDirClean.startsWith(userDirClean,cs)) {
            return tr("The local folder %1 already contains a folder used in a folder sync connection. "
                      "Please pick another one!")
                    .arg(QDir::toNativeSeparators(path));
        }

        // QDir::cleanPath keeps links
        // canonicalPath() remove symlinks and uses the symlink targets.
        QString absCleanUserFolder = QDir::cleanPath(QDir(path).canonicalPath())+'/';

        if ( (forNewDirectory || differentPathes) && userDirClean.startsWith( folderDirClean, cs )) {
            return tr("The local folder %1 is already contained in a folder used in a folder sync connection. "
                      "Please pick another one!")
                    .arg(QDir::toNativeSeparators(path));
        }

        // both follow symlinks.
        bool cleanUserEqualsCleanFolder = Utility::fileNamesEqual(absCleanUserFolder, folderDirClean );
        if (differentPathes && absCleanUserFolder.startsWith( folderDirClean, cs ) &&
                ! cleanUserEqualsCleanFolder ) {
            return tr("The local folder %1 is a symbolic link. "
                      "The link target is already contained in a folder used in a folder sync connection. "
                      "Please pick another one!")
                    .arg(QDir::toNativeSeparators(path));
        }

        if (differentPathes && folderDirClean.startsWith(absCleanUserFolder, cs) &&
                !cleanUserEqualsCleanFolder && !forNewDirectory ) {
            return tr("The local folder %1 contains a symbolic link. "
                      "The link target contains an already synced folder "
                      "Please pick another one!")
                    .arg(QDir::toNativeSeparators(path));
        }

        // if both pathes are equal, the server url needs to be different
        // otherwise it would mean that a new connection from the same local folder
        // to the same account is added which is not wanted. The account must differ.
        if( serverUrl.isValid() && Utility::fileNamesEqual(absCleanUserFolder,folderDir ) ) {
            QUrl folderUrl = f->accountState()->account()->url();
            QString user = f->accountState()->account()->credentials()->user();
            folderUrl.setUserName(user);

            if( serverUrl == folderUrl ) {
                return tr("There is already a sync from the server to this local folder. "
                          "Please pick another local folder!");
            }
        }
    }

    return QString();

}

bool FolderMan::ignoreHiddenFiles() const
{
    if (_folderMap.empty()) {
        return true;
    }
    return _folderMap.begin().value()->ignoreHiddenFiles();
}

void FolderMan::setIgnoreHiddenFiles(bool ignore)
{
    // Note that the setting will revert to 'true' if all folders
    // are deleted...
    foreach (Folder* folder, _folderMap) {
        folder->setIgnoreHiddenFiles(ignore);
        folder->saveToSettings();
    }
}

QQueue<Folder*> FolderMan::scheduleQueue() const
{
    return _scheduledFolders;
}

Folder *FolderMan::currentSyncFolder() const
{
    return _currentSyncFolder;
}

void FolderMan::restartApplication()
{
    if( Utility::isLinux() ) {
        // restart:
        qDebug() << "### Restarting application NOW, PID" << qApp->applicationPid() << "is ending.";
        qApp->quit();
        QStringList args = qApp->arguments();
        QString prg = args.takeFirst();

        QProcess::startDetached(prg, args);
    } else {
        qDebug() << "On this platform we do not restart.";
    }
}

} // namespace OCC
