/*
 * 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 "config.h"

#include "account.h"
#include "accountstate.h"
#include "folder.h"
#include "folderman.h"
#include "logger.h"
#include "configfile.h"
#include "networkjobs.h"
#include "syncjournalfilerecord.h"
#include "syncresult.h"
#include "utility.h"
#include "clientproxy.h"
#include "syncengine.h"
#include "syncrunfilelog.h"
#include "socketapi.h"
#include "theme.h"
#include "filesystem.h"
#include "excludedfiles.h"

#include "creds/abstractcredentials.h"

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

#include <QMessageBox>
#include <QPushButton>

namespace OCC {

static void csyncLogCatcher(int /*verbosity*/,
                        const char */*function*/,
                        const char *buffer,
                        void */*userdata*/)
{
    Logger::instance()->csyncLog( QString::fromUtf8(buffer) );
}


Folder::Folder(const FolderDefinition& definition,
               QObject* parent)
    : QObject(parent)
      , _accountState(0)
      , _definition(definition)
      , _csyncError(false)
      , _csyncUnavail(false)
      , _wipeDb(false)
      , _proxyDirty(true)
      , _lastSyncDuration(0)
      , _forceSyncOnPollTimeout(false)
      , _consecutiveFailingSyncs(0)
      , _consecutiveFollowUpSyncs(0)
      , _journal(definition.localPath)
      , _csync_ctx(0)
{
    qsrand(QTime::currentTime().msec());
    _timeSinceLastSyncStart.start();
    _timeSinceLastSyncDone.start();

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

    // check if the local path exists
    checkLocalPath();

    _syncResult.setFolder(_definition.alias);
}

bool Folder::init()
{
    // We need to reconstruct the url because the path needs to be fully decoded, as csync will re-encode the path:
    //  Remember that csync will just append the filename to the path and pass it to the vio plugin.
    //  csync_owncloud will then re-encode everything.
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
    QUrl url = remoteUrl();
    QString url_string = url.scheme() + QLatin1String("://") + url.authority(QUrl::EncodeDelimiters) + url.path(QUrl::FullyDecoded);
#else
    // Qt4 was broken anyway as it did not encode the '#' as it should have done  (it was actually a problem when parsing the path from QUrl::setPath
    QString url_string = remoteUrl().toString();
#endif
    url_string = Utility::toCSyncScheme(url_string);

    QString localpath = path();

    if( csync_create( &_csync_ctx, localpath.toUtf8().data(), url_string.toUtf8().data() ) < 0 ) {
        qDebug() << "Unable to create csync-context!";
        slotSyncError(tr("Unable to create csync-context"));
        _csync_ctx = 0;
    } else {
        csync_set_log_callback( csyncLogCatcher );
        csync_set_log_level( Logger::instance()->isNoop() ? 0 : 11 );

        Q_ASSERT( _accountState );

        if( csync_init( _csync_ctx ) < 0 ) {
            qDebug() << "Could not initialize csync!" << csync_get_status(_csync_ctx) << csync_get_status_string(_csync_ctx);
            QString errStr = SyncEngine::csyncErrorToString(CSYNC_STATUS(csync_get_status(_csync_ctx)));
            const char *errMsg = csync_get_status_string(_csync_ctx);
            if( errMsg ) {
                errStr += QLatin1String("<br/>");
                errStr += QString::fromUtf8(errMsg);
            }
            slotSyncError(errStr);
            csync_destroy(_csync_ctx);
            _csync_ctx = 0;
        }
    }
    return _csync_ctx;
}

Folder::~Folder()
{
    if( _engine ) {
        _engine->abort();
        _engine.reset(0);
    }
    // Destroy csync here.
    csync_destroy(_csync_ctx);
}

void Folder::setAccountState( AccountState *account )
{
    _accountState = account;
}

AccountState* Folder::accountState() const
{
    return _accountState;
}

void Folder::checkLocalPath()
{
    const QFileInfo fi(_definition.localPath);

    if( fi.isDir() && fi.isReadable() ) {
        qDebug() << "Checked local path ok";
    } else {
        // Check directory again
        if( !FileSystem::fileExists(_definition.localPath, fi) ) {
            _syncResult.setErrorString(tr("Local folder %1 does not exist.").arg(_definition.localPath));
            _syncResult.setStatus( SyncResult::SetupError );
        } else if( !fi.isDir() ) {
            _syncResult.setErrorString(tr("%1 should be a folder but is not.").arg(_definition.localPath));
            _syncResult.setStatus( SyncResult::SetupError );
        } else if( !fi.isReadable() ) {
            _syncResult.setErrorString(tr("%1 is not readable.").arg(_definition.localPath));
            _syncResult.setStatus( SyncResult::SetupError );
        }
    }
}

QString Folder::aliasGui() const
{
    if (remotePath().length() > 0 && remotePath() != QLatin1String("/")) {
        QString a = QFile(remotePath()).fileName();
        if (a.startsWith('/')) {
            a = a.remove(0, 1);
        }
        return a;
    } else {
        return Theme::instance()->appNameGUI();
    }
}

QString Folder::alias() const
{
    return _definition.alias;
}

QString Folder::path() const
{
    QString p(_definition.localPath);
    if( ! p.endsWith('/') ) {
        p.append('/');
    }
    return p;
}

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


bool Folder::ignoreHiddenFiles()
{
    bool re(_definition.ignoreHiddenFiles);
    return re;
}

void Folder::setIgnoreHiddenFiles(bool ignore)
{
    _definition.ignoreHiddenFiles = ignore;
}

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

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

    return cleanedPath;
}

bool Folder::isBusy() const
{
    return !_engine.isNull();
}

QString Folder::remotePath() const
{
    return _definition.targetPath;
}

QUrl Folder::remoteUrl() const
{
    if (!_accountState) {
        return QUrl("http://deleted-account");
    }
    return Account::concatUrlPath(_accountState->account()->davUrl(), remotePath());
}

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

bool Folder::canSync() const
{
    return !syncPaused() && accountState()->isConnected();
}

void Folder::setSyncPaused( bool paused )
{
    if (paused != _definition.paused) {
        _definition.paused = paused;
        saveToSettings();
    }

    if( !paused ) {
        // qDebug() << "Syncing enabled on folder " << name();
    } else {
        // do not stop or start the watcher here, that is done internally by
        // folder class. Even if the watcher fires, the folder does not
        // schedule itself because it checks the var. _enabled before.
        setSyncState(SyncResult::Paused);
    }
}

void Folder::setSyncState(SyncResult::Status state)
{
    _syncResult.setStatus(state);
}

SyncResult Folder::syncResult() const
{
  return _syncResult;
}

void Folder::prepareToSync()
{
    _syncResult.setStatus( SyncResult::NotYetStarted );
    _syncResult.clearErrors();
}

void Folder::slotRunEtagJob()
{
    qDebug() << "* Trying to check" << remoteUrl().toString() << "for changes via ETag check. (time since last sync:" << (_timeSinceLastSyncDone.elapsed() / 1000) << "s)";

    if (!_accountState) {
        qDebug() << "Can't run EtagJob, account is deleted";
        return;
    }

    AccountPtr account = _accountState->account();

    if (!_requestEtagJob.isNull()) {
        qDebug() << Q_FUNC_INFO << remoteUrl().toString() << "has ETag job queued, not trying to sync";
        return;
    }

    if (_definition.paused || !_accountState->isConnected()) {
        qDebug() << "Not syncing.  :"  << remoteUrl().toString() << _definition.paused << AccountState::stateString(_accountState->state());
        return;
    }

    bool forceSyncIntervalExpired =
            quint64(_timeSinceLastSyncDone.elapsed()) > ConfigFile().forceSyncInterval();
    bool syncAgainAfterFail = _consecutiveFailingSyncs > 0 && _consecutiveFailingSyncs < 3;

    // There are several conditions under which we trigger a full-discovery sync:
    // * When a suitably long time has passed since the last sync finished
    // * When the last sync failed (only a couple of times)
    // * When the last sync requested another sync to be done (only a couple of times)
    //
    // Note that the etag check (see below) and the file watcher may also trigger
    // syncs.
    if (forceSyncIntervalExpired
            || _forceSyncOnPollTimeout
            || syncAgainAfterFail) {

        if (forceSyncIntervalExpired) {
            qDebug() << "** Force Sync, because it has been " << _timeSinceLastSyncDone.elapsed() << "ms "
                     << "since the last sync";
        }
        if (_forceSyncOnPollTimeout) {
            qDebug() << "** Force Sync, because it was requested";
        }
        if (syncAgainAfterFail) {
            qDebug() << "** Force Sync, because the last"
                     << _consecutiveFailingSyncs << "syncs failed, last status:"
                     << _syncResult.statusString();
        }
        _forceSyncOnPollTimeout = false;
        emit scheduleToSync(this);

    } else {
        // Do the ordinary etag check for the root folder and only schedule a real
        // sync if it's different.

        _requestEtagJob = new RequestEtagJob(account, remotePath(), this);
        // check if the etag is different
        QObject::connect(_requestEtagJob, SIGNAL(etagRetreived(QString)), this, SLOT(etagRetreived(QString)));
        FolderMan::instance()->slotScheduleETagJob(alias(), _requestEtagJob);
        // The _requestEtagJob is auto deleting itself on finish. Our guard pointer _requestEtagJob will then be null.
    }
}

void Folder::etagRetreived(const QString& etag)
{
    //qDebug() << "* Compare etag with previous etag: last:" << _lastEtag << ", received:" << etag;

    // re-enable sync if it was disabled because network was down
    FolderMan::instance()->setSyncEnabled(true);

    if (_lastEtag != etag) {
        qDebug() << "* Compare etag with previous etag: last:" << _lastEtag << ", received:" << etag << "-> CHANGED";
        _lastEtag = etag;
        emit scheduleToSync(this);
    }

    if( _accountState ) {
        _accountState->tagLastSuccessfullETagRequest();
    }
}

void Folder::etagRetreivedFromSyncEngine(const QString& etag)
{
    qDebug() << "Root etag from during sync:" << etag;
    accountState()->tagLastSuccessfullETagRequest();
    _lastEtag = etag;
}


void Folder::bubbleUpSyncResult()
{
    // count new, removed and updated items
    int newItems = 0;
    int removedItems = 0;
    int updatedItems = 0;
    int ignoredItems = 0;
    int renamedItems = 0;
    int errorItems = 0;

    SyncFileItemPtr firstItemNew;
    SyncFileItemPtr firstItemDeleted;
    SyncFileItemPtr firstItemUpdated;
    SyncFileItemPtr firstItemRenamed;
    SyncFileItemPtr firstItemError;

    SyncRunFileLog syncFileLog;

    syncFileLog.start(path(), _engine ? _engine->stopWatch() : Utility::StopWatch() );

    QElapsedTimer timer;
    timer.start();

    foreach (const SyncFileItemPtr &item, _syncResult.syncFileItemVector() ) {
        // Log the item
        syncFileLog.logItem( *item );

        // and process the item to the gui
        if( item->_status == SyncFileItem::FatalError || item->_status == SyncFileItem::NormalError ) {
            slotSyncError( tr("%1: %2").arg(item->_file, item->_errorString) );
            errorItems++;
            if (!firstItemError) {
                firstItemError = item;
            }
        } else if( item->_status == SyncFileItem::FileIgnored ) {
            // ignored files don't show up in notifications
            continue;
        } else {
            // add new directories or remove gone away dirs to the watcher
            if (item->_isDirectory && item->_instruction == CSYNC_INSTRUCTION_NEW ) {
                FolderMan::instance()->addMonitorPath( alias(), path()+item->_file );
            }
            if (item->_isDirectory && item->_instruction == CSYNC_INSTRUCTION_REMOVE ) {
                FolderMan::instance()->removeMonitorPath( alias(), path()+item->_file );
            }

            if (!item->hasErrorStatus() && item->_direction == SyncFileItem::Down) {
                switch (item->_instruction) {
                case CSYNC_INSTRUCTION_NEW:
                case CSYNC_INSTRUCTION_TYPE_CHANGE:
                    newItems++;
                    if (!firstItemNew)
                        firstItemNew = item;
                    break;
                case CSYNC_INSTRUCTION_REMOVE:
                    removedItems++;
                    if (!firstItemDeleted)
                        firstItemDeleted = item;
                    break;
                case CSYNC_INSTRUCTION_CONFLICT:
                case CSYNC_INSTRUCTION_SYNC:
                    if (!item->_isDirectory) {
                        updatedItems++;
                        if (!firstItemUpdated)
                            firstItemUpdated = item;
                    }
                    break;
                case CSYNC_INSTRUCTION_ERROR:
                    qDebug() << "Got Instruction ERROR. " << _syncResult.errorString();
                    break;
                case CSYNC_INSTRUCTION_RENAME:
                    if (!firstItemRenamed) {
                        firstItemRenamed = item;
                    }
                    renamedItems++;
                    break;
                default:
                    // nothing.
                    break;
                }
            } else if( item->_direction == SyncFileItem::None ) { // ignored files counting.
                if( item->_instruction == CSYNC_INSTRUCTION_IGNORE ) {
                    ignoredItems++;
                }
            }
        }
    }
    syncFileLog.close();

    qDebug() << "Processing result list and logging took " << timer.elapsed() << " Milliseconds.";
    _syncResult.setWarnCount(ignoredItems);

    if( firstItemNew ) {
        createGuiLog( firstItemNew->_file,     SyncFileStatus::STATUS_NEW, newItems );
    }
    if( firstItemDeleted ) {
        createGuiLog( firstItemDeleted->_file, SyncFileStatus::STATUS_REMOVE, removedItems );
    }
    if( firstItemUpdated ) {
        createGuiLog( firstItemUpdated->_file, SyncFileStatus::STATUS_UPDATED, updatedItems );
    }

    if( firstItemRenamed ) {
        SyncFileStatus status(SyncFileStatus::STATUS_RENAME);
        // if the path changes it's rather a move
        QDir renTarget = QFileInfo(firstItemRenamed->_renameTarget).dir();
        QDir renSource = QFileInfo(firstItemRenamed->_file).dir();
        if(renTarget != renSource) {
            status.set(SyncFileStatus::STATUS_MOVE);
        }
        createGuiLog( firstItemRenamed->_originalFile, status, renamedItems, firstItemRenamed->_renameTarget );
    }

    createGuiLog( firstItemError->_file,   SyncFileStatus::STATUS_ERROR, errorItems );

    qDebug() << "OO folder slotSyncFinished: result: " << int(_syncResult.status());
}

void Folder::createGuiLog( const QString& filename, SyncFileStatus status, int count,
                           const QString& renameTarget )
{
    if(count > 0) {
        Logger *logger = Logger::instance();

        QString file = QDir::toNativeSeparators(filename);
        QString text;

        // not all possible values of status are evaluated here because the others
        // are not used in the calling function. Please check there.
        switch (status.tag()) {
        case SyncFileStatus::STATUS_REMOVE:
            if( count > 1 ) {
                text = tr("%1 and %2 other files have been removed.", "%1 names a file.").arg(file).arg(count-1);
            } else {
                text = tr("%1 has been removed.", "%1 names a file.").arg(file);
            }
            break;
        case SyncFileStatus::STATUS_NEW:
            if( count > 1 ) {
                text = tr("%1 and %2 other files have been downloaded.", "%1 names a file.").arg(file).arg(count-1);
            } else {
                text = tr("%1 has been downloaded.", "%1 names a file.").arg(file);
            }
            break;
        case SyncFileStatus::STATUS_UPDATED:
            if( count > 1 ) {
                text = tr("%1 and %2 other files have been updated.").arg(file).arg(count-1);
            } else {
                text = tr("%1 has been updated.", "%1 names a file.").arg(file);
            }
            break;
        case SyncFileStatus::STATUS_RENAME:
            if( count > 1 ) {
                text = tr("%1 has been renamed to %2 and %3 other files have been renamed.").arg(file).arg(renameTarget).arg(count-1);
            } else {
                text = tr("%1 has been renamed to %2.", "%1 and %2 name files.").arg(file).arg(renameTarget);
            }
            break;
        case SyncFileStatus::STATUS_MOVE:
            if( count > 1 ) {
                text = tr("%1 has been moved to %2 and %3 other files have been moved.").arg(file).arg(renameTarget).arg(count-1);
            } else {
                text = tr("%1 has been moved to %2.").arg(file).arg(renameTarget);
            }
            break;
        case SyncFileStatus::STATUS_ERROR:
            if( count > 1 ) {
                text = tr("%1 and %2 other files could not be synced due to errors. See the log for details.", "%1 names a file.").arg(file).arg(count-1);
            } else {
                text = tr("%1 could not be synced due to an error. See the log for details.").arg(file);
            }
            break;
        default:
            break;
        }

        if( !text.isEmpty() ) {
            logger->postOptionalGuiLog( tr("Sync Activity"), text );
        }
    }
}

int 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);
    foreach (const SyncJournalDb::DownloadInfo & deleted_info, deleted_infos) {
        const QString tmppath = folderpath.filePath(deleted_info._tmpfile);
        qDebug() << "Deleting temporary file: " << tmppath;
        FileSystem::remove(tmppath);
    }
    return deleted_infos.size();
}

int Folder::downloadInfoCount()
{
    return _journal.downloadInfoCount();
}

int Folder::errorBlackListEntryCount()
{
    return _journal.errorBlackListEntryCount();
}

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

void Folder::slotWatchedPathChanged(const QString& path)
{
    // When no sync is running or it's in the prepare phase, we can
    // always schedule a new sync.
    if (! _engine || _syncResult.status() == SyncResult::SyncPrepare) {
        emit scheduleToSync(this);
        return;
    }

    // The folder watcher fires a lot of bogus notifications during
    // a sync operation, both for actual user files and the database
    // and log. Therefore we check notifications against operations
    // the sync is doing to filter out our own changes.
    bool ownChange = false;
#ifdef Q_OS_MAC
    Q_UNUSED(path)
    // On OSX the folder watcher does not report changes done by our
    // own process. Therefore nothing needs to be done here!
#else
    // Use the path to figure out whether it was our own change
    const auto maxNotificationDelay = 15*1000;
    qint64 time = _engine->timeSinceFileTouched(path);
    if (time != -1 && time < maxNotificationDelay) {
        ownChange = true;
    }
#endif

    if (! ownChange) {
        emit scheduleToSync(this);
    }
}

/**
 * Whether this item should get an ERROR icon through the Socket API.
 *
 * The Socket API should only present serious, permanent errors to the user.
 * In particular SoftErrors should just retain their 'needs to be synced'
 * icon as the problem is most likely going to resolve itself quickly and
 * automatically.
 */
static bool showErrorInSocketApi(const SyncFileItem& item)
{
    const auto status = item._status;
    return status == SyncFileItem::NormalError
        || status == SyncFileItem::FatalError;
}

static void addErroredSyncItemPathsToList(const SyncFileItemVector& items, QSet<QString>* set) {
    foreach (const SyncFileItemPtr &item, items) {
        if (showErrorInSocketApi(*item)) {
            set->insert(item->_file);
        }
    }
}

void Folder::slotThreadTreeWalkResult(const SyncFileItemVector& items)
{
    addErroredSyncItemPathsToList(items, &this->_stateLastSyncItemsWithErrorNew);
    _syncResult.setSyncFileItemVector(items);
}

void Folder::slotAboutToPropagate(SyncFileItemVector& items)
{
    // empty the tainted list since the status generation code will use the _syncedItems
    // (which imply the folder) to generate the syncing state icon now.
    _stateTaintedFolders.clear();

    addErroredSyncItemPathsToList(items, &this->_stateLastSyncItemsWithErrorNew);
}


bool Folder::estimateState(QString fn, csync_ftw_type_e t, SyncFileStatus* s)
{
    if (t == CSYNC_FTW_TYPE_DIR) {
        if (Utility::doesSetContainPrefix(_stateLastSyncItemsWithError, fn)) {
            qDebug() << Q_FUNC_INFO << "Folder has error" << fn;
            s->set(SyncFileStatus::STATUS_ERROR);
            return true;
        }
        // If sync is running, check _syncedItems, possibly give it STATUS_EVAL (=syncing down)
        if (!_engine.isNull()) {
            if (_engine->estimateState(fn, t, s)) {
                return true;
            }
        }
        if(!fn.endsWith(QLatin1Char('/'))) {
            fn.append(QLatin1Char('/'));
        }
        if (Utility::doesSetContainPrefix(_stateTaintedFolders, fn)) {
            qDebug() << Q_FUNC_INFO << "Folder is tainted, EVAL!" << fn;
            s->set(SyncFileStatus::STATUS_EVAL);
            return true;
        }
        return false;
    } else if ( t== CSYNC_FTW_TYPE_FILE) {
        // check if errorList has the directory/file
        if (Utility::doesSetContainPrefix(_stateLastSyncItemsWithError, fn)) {
            s->set(SyncFileStatus::STATUS_ERROR);
            return true;
        }
        // If sync running: _syncedItems -> SyncingState
        if (!_engine.isNull()) {
            if (_engine->estimateState(fn, t, s)) {
                return true;
            }
        }
    }
    return false;
}

void Folder::saveToSettings() const
{
    if (!_accountState) {
        qDebug() << "Can't save folder to settings, account is deleted";
        return;
    }

    auto settings = _accountState->settings();
    settings->beginGroup(QLatin1String("Folders"));
    FolderDefinition::save(*settings, _definition);

    settings->sync();
    qDebug() << "Saved folder" << _definition.alias << "to settings, status" << settings->status();
}

void Folder::removeFromSettings() const
{
    if (!_accountState) {
        qDebug() << "Can't remove folder from settings, account is deleted";
        return;
    }

    auto  settings = _accountState->settings();
    settings->beginGroup(QLatin1String("Folders"));
    settings->remove(_definition.alias);
}

bool Folder::isFileExcludedAbsolute(const QString& fullPath) const
{
    if (!fullPath.startsWith(path())) {
        // Mark paths we're not responsible for as excluded...
        return true;
    }

    QString myFullPath = fullPath;
    if (myFullPath.endsWith(QLatin1Char('/'))) {
        myFullPath.chop(1);
    }

    QString relativePath = myFullPath.mid(path().size());
    auto excl = ExcludedFiles::instance().isExcluded(myFullPath, relativePath, _definition.ignoreHiddenFiles);
    return excl != CSYNC_NOT_EXCLUDED;
}

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

void Folder::watcherSlot(QString fn)
{
    // FIXME: On OS X we could not do this "if" since on OS X the file watcher ignores events for ourselves
    // however to have the same behaviour atm on all platforms, we don't do it
    if (!_engine.isNull()) {
        qDebug() << Q_FUNC_INFO << "Sync running, IGNORE event for " << fn;
        return;
    }
    QFileInfo fi(fn);
    if (fi.isFile()) {
        fn = fi.path(); // depending on OS, file watcher might be for dir or file
    }
    // Make it a relative path depending on the folder
    QString relativePath = fn.remove(0, path().length());
    if( !relativePath.endsWith(QLatin1Char('/'))) {
        relativePath.append(QLatin1Char('/'));
    }
    qDebug() << Q_FUNC_INFO << fi.canonicalFilePath() << fn << relativePath;
    _stateTaintedFolders.insert(relativePath);

    // Notify the SocketAPI?
}



void Folder::slotTerminateSync()
{
    qDebug() << "folder " << alias() << " Terminating!";

    if( _engine ) {
        _engine->abort();

        // Do not display an error message, user knows his own actions.
        // _errors.append( tr("The CSync thread terminated.") );
        // _csyncError = true;
        setSyncState(SyncResult::SyncAbortRequested);
    }
}

// This removes the csync File database
// This is needed to provide a clean startup again in case another
// local folder is synced to the same ownCloud.
void Folder::wipe()
{
    QString stateDbFile = path()+QLatin1String(".csync_journal.db");

    // 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(alias());
    _journal.close(); // close the sync journal

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

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

    FolderMan::instance()->socketApi()->slotRegisterPath(alias());
}

bool Folder::setIgnoredFiles()
{
    bool ok = false;

    ConfigFile cfgFile;
    csync_clear_exclude_list( _csync_ctx );
    QString excludeList = cfgFile.excludeFile( ConfigFile::SystemScope );
    if( !excludeList.isEmpty() ) {
        qDebug() << "==== added system ignore list to csync:" << excludeList.toUtf8();
        if (csync_add_exclude_list( _csync_ctx, excludeList.toUtf8() ) == 0) {
            ok = true;
        }
    }
    excludeList = cfgFile.excludeFile( ConfigFile::UserScope );
    if( !excludeList.isEmpty() ) {
        qDebug() << "==== added user defined ignore list to csync:" << excludeList.toUtf8();
        csync_add_exclude_list( _csync_ctx, excludeList.toUtf8() );
        // reading the user exclude file is optional
    }

    return ok;
}

void Folder::setProxyDirty(bool value)
{
    _proxyDirty = value;
}

bool Folder::proxyDirty()
{
    return _proxyDirty;
}

void Folder::startSync(const QStringList &pathList)
{
    if (!_accountState) {
        qDebug() << "Can't startSync, account is deleted";
        return;
    }

    Q_UNUSED(pathList)
    if (!_csync_ctx) {
        // no _csync_ctx yet,  initialize it.
        init();

        if (!_csync_ctx) {
            qDebug() << Q_FUNC_INFO << "init failed.";
            // the error should already be set
            QMetaObject::invokeMethod(this, "slotSyncFinished", Qt::QueuedConnection, Q_ARG(bool, false));
            return;
        }
    } else if (proxyDirty()) {
        setProxyDirty(false);
    }
    csync_set_log_level( Logger::instance()->isNoop() ? 0 : 11 );

    if (isBusy()) {
        qCritical() << "* ERROR csync is still running and new sync requested.";
        return;
    }
    _errors.clear();
    _csyncError = false;
    _csyncUnavail = false;

    _timeSinceLastSyncStart.restart();
    _syncResult.clearErrors();
    _syncResult.setStatus( SyncResult::SyncPrepare );
    _syncResult.setSyncFileItemVector(SyncFileItemVector());
    emit syncStateChange();

    qDebug() << "*** Start syncing " << remoteUrl().toString() << " - client version"
             << qPrintable(Theme::instance()->version());

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

    // pass the setting if hidden files are to be ignored, will be read in csync_update
    _csync_ctx->ignore_hidden_files = _definition.ignoreHiddenFiles;

    _engine.reset(new SyncEngine( _accountState->account(), _csync_ctx, path(), remoteUrl().path(), remotePath(), &_journal));

    qRegisterMetaType<SyncFileItemVector>("SyncFileItemVector");
    qRegisterMetaType<SyncFileItem::Direction>("SyncFileItem::Direction");

    connect(_engine.data(), SIGNAL(rootEtag(QString)), this, SLOT(etagRetreivedFromSyncEngine(QString)));
    connect( _engine.data(), SIGNAL(treeWalkResult(const SyncFileItemVector&)),
              this, SLOT(slotThreadTreeWalkResult(const SyncFileItemVector&)), Qt::QueuedConnection);
    connect( _engine.data(), SIGNAL(aboutToPropagate(SyncFileItemVector&)),
              this, SLOT(slotAboutToPropagate(SyncFileItemVector&)));

    connect(_engine.data(), SIGNAL(started()),  SLOT(slotSyncStarted()), Qt::QueuedConnection);
    connect(_engine.data(), SIGNAL(finished(bool)), SLOT(slotSyncFinished(bool)), Qt::QueuedConnection);
    connect(_engine.data(), SIGNAL(csyncError(QString)), SLOT(slotSyncError(QString)), Qt::QueuedConnection);
    connect(_engine.data(), SIGNAL(csyncUnavailable()), SLOT(slotCsyncUnavailable()), Qt::QueuedConnection);

    //direct connection so the message box is blocking the sync.
    connect(_engine.data(), SIGNAL(aboutToRemoveAllFiles(SyncFileItem::Direction,bool*)),
                    SLOT(slotAboutToRemoveAllFiles(SyncFileItem::Direction,bool*)));
    connect(_engine.data(), SIGNAL(folderDiscovered(bool,QString)), this, SLOT(slotFolderDiscovered(bool,QString)));
    connect(_engine.data(), SIGNAL(transmissionProgress(ProgressInfo)), this, SLOT(slotTransmissionProgress(ProgressInfo)));
    connect(_engine.data(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)),
            this, SLOT(slotItemCompleted(const SyncFileItem &, const PropagatorJob &)));
    connect(_engine.data(), SIGNAL(syncItemDiscovered(const SyncFileItem &)), this, SLOT(slotSyncItemDiscovered(const SyncFileItem &)));
    connect(_engine.data(), SIGNAL(newBigFolder(QString)), this, SLOT(slotNewBigFolderDiscovered(QString)));

    setDirtyNetworkLimits();

    ConfigFile cfgFile;
    auto newFolderLimit = cfgFile.newBigFolderSizeLimit();
    quint64 limit = newFolderLimit.first ? newFolderLimit.second * 1000 * 1000 : -1; // convert from MB to B
    _engine->setNewBigFolderSizeLimit(limit);

    QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection);

    // disable events until syncing is done
    // _watcher->setEventsEnabled(false);
    emit syncStarted();
}

void Folder::setDirtyNetworkLimits()
{
    if (_engine) {

        ConfigFile cfg;
        int downloadLimit = -75; // 75%
        int useDownLimit = cfg.useDownloadLimit();
        if (useDownLimit >= 1) {
            downloadLimit = cfg.downloadLimit() * 1000;
        } else if (useDownLimit == 0) {
            downloadLimit = 0;
        }

        int uploadLimit = -75; // 75%
        int useUpLimit = cfg.useUploadLimit();
        if ( useUpLimit >= 1) {
            uploadLimit = cfg.uploadLimit() * 1000;
        } else if (useUpLimit == 0) {
            uploadLimit = 0;
        }

        _engine->setNetworkLimits(uploadLimit, downloadLimit);
    }
}


void Folder::slotSyncError(const QString& err)
{
    _errors.append( err );
    _csyncError = true;
}

void Folder::slotSyncStarted()
{
    qDebug() << "#### Propagation start #################################################### >>";
    _syncResult.setStatus(SyncResult::SyncRunning);
    emit syncStateChange();
}

void Folder::slotCsyncUnavailable()
{
    _csyncUnavail = true;
}

void Folder::slotSyncFinished(bool success)
{
    qDebug() << " - client version" << qPrintable(Theme::instance()->version())
             <<  " Qt" << qVersion()
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
              <<  " SSL " <<  QSslSocket::sslLibraryVersionString().toUtf8().data()
#endif
   ;


    if( _csyncError ) {
        qDebug() << "-> SyncEngine finished with ERROR, warn count is" << _syncResult.warnCount();
    } else {
        qDebug() << "-> SyncEngine finished without problem.";
    }
    bubbleUpSyncResult();

    bool anotherSyncNeeded = false;
    if (_engine) {
        anotherSyncNeeded = _engine->isAnotherSyncNeeded();
        _engine.reset(0);
    }
    // _watcher->setEventsEnabledDelayed(2000);



    // This is for sync state calculation
    _stateLastSyncItemsWithError = _stateLastSyncItemsWithErrorNew;
    _stateLastSyncItemsWithErrorNew.clear();
    _stateTaintedFolders.clear(); // heuristic: assume the sync had been done, new file watches needed to taint dirs

    if (_csyncError) {
        _syncResult.setStatus(SyncResult::Error);
        qDebug() << "  ** error Strings: " << _errors;
        _syncResult.setErrorStrings( _errors );
        qDebug() << "    * owncloud csync thread finished with error";
    } else if (_csyncUnavail) {
        _syncResult.setStatus(SyncResult::Error);
        qDebug() << "  ** csync not available.";
    } else if( _syncResult.warnCount() > 0 ) {
        // there have been warnings on the way.
        _syncResult.setStatus(SyncResult::Problem);
    } else {
        _syncResult.setStatus(SyncResult::Success);
    }

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

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

    emit syncStateChange();

    // The syncFinished result that is to be triggered here makes the folderman
    // clear the current running sync folder marker.
    // Lets wait a bit to do that because, as long as this marker is not cleared,
    // file system change notifications are ignored for that folder. And it takes
    // some time under certain conditions to make the file system notifications
    // all come in.
    QTimer::singleShot(200, this, SLOT(slotEmitFinishedDelayed() ));

    _lastSyncDuration = _timeSinceLastSyncStart.elapsed();
    _timeSinceLastSyncDone.restart();

    // Increment the follow-up sync counter if necessary.
    if (anotherSyncNeeded) {
        _consecutiveFollowUpSyncs++;
        qDebug() << "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 && _consecutiveFollowUpSyncs <= 3)
    {
        _forceSyncOnPollTimeout = true;
        // We will make sure that the poll timer occurs soon enough.
        // delay 1s, 4s, 9s
        int c = _consecutiveFollowUpSyncs;
        QTimer::singleShot(c*c * 1000, this, SLOT(slotRunEtagJob() ));
    }
}

void Folder::slotEmitFinishedDelayed()
{
    emit syncFinished( _syncResult );
}


void Folder::slotFolderDiscovered(bool, QString folderName)
{
    ProgressInfo pi;
    pi._currentDiscoveredFolder = folderName;
    emit progressInfo(pi);
    ProgressDispatcher::instance()->setProgressInfo(alias(), pi);
}


// the progress comes without a folder and the valid path set. Add that here
// and hand the result over to the progress dispatcher.
void Folder::slotTransmissionProgress(const ProgressInfo &pi)
{
    if( !pi.hasStarted() ) {
        // this is the beginning of a sync, set the warning level to 0
        _syncResult.setWarnCount(0);
    }
    emit progressInfo(pi);
    ProgressDispatcher::instance()->setProgressInfo(alias(), pi);
}

// a item is completed: count the errors and forward to the ProgressDispatcher
void Folder::slotItemCompleted(const SyncFileItem &item, const PropagatorJob& job)
{
    if (showErrorInSocketApi(item)) {
        _stateLastSyncItemsWithErrorNew.insert(item._file);
    }

    if (Progress::isWarningKind(item._status)) {
        // Count all error conditions.
        _syncResult.setWarnCount(_syncResult.warnCount()+1);
    }
    emit ProgressDispatcher::instance()->itemCompleted(alias(), item, job);
}

void Folder::slotSyncItemDiscovered(const SyncFileItem & item)
{
    emit ProgressDispatcher::instance()->syncItemDiscovered(alias(), item);
}

void Folder::slotNewBigFolderDiscovered(const QString &newF)
{
    auto newFolder = newF;
    if (!newFolder.endsWith(QLatin1Char('/'))) {
        newFolder += QLatin1Char('/');
    }
    auto journal = journalDb();

    // Add the entry to the blacklist if it is neither in the blacklist or whitelist already
    auto blacklist = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList);
    auto whitelist = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList);
    if (!blacklist.contains(newFolder) && !whitelist.contains(newFolder)) {
        blacklist.append(newFolder);
        journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blacklist);
    }

    // And add the entry to the undecided list and signal the UI
    auto undecidedList = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList);
    if (!undecidedList.contains(newFolder)) {
        undecidedList.append(newFolder);
        journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, undecidedList);
        emit newBigFolderDiscovered(newFolder);
    }
    QString message = tr("A new folder larger than %1 MB has been added: %2.\n"
                         "Please go in the settings to select it if you wish to download it.")
                .arg(ConfigFile().newBigFolderSizeLimit().second).arg(newF);

    auto logger = Logger::instance();
    logger->postOptionalGuiLog(Theme::instance()->appNameGUI(), message);
}



void Folder::slotAboutToRemoveAllFiles(SyncFileItem::Direction, bool *cancel)
{
    QString msg =
        tr("This sync would remove all the files in the sync folder '%1'.\n"
           "This might be because the folder was silently reconfigured, or that all "
           "the files were manually removed.\n"
           "Are you sure you want to perform this operation?");
    QMessageBox msgBox(QMessageBox::Warning, tr("Remove All Files?"),
                       msg.arg(alias()));
    msgBox.addButton(tr("Remove all files"), QMessageBox::DestructiveRole);
    QPushButton* keepBtn = msgBox.addButton(tr("Keep files"), QMessageBox::AcceptRole);
    if (msgBox.exec() == -1) {
        *cancel = true;
        return;
    }
    *cancel = msgBox.clickedButton() == keepBtn;
    if (*cancel) {
        wipe();
        // speed up next sync
        _lastEtag.clear();
        _forceSyncOnPollTimeout = true;
        QTimer::singleShot(50, this, SLOT(slotRunEtagJob()));
    }
}



void FolderDefinition::save(QSettings& settings, const FolderDefinition& folder)
{
    settings.beginGroup(FolderMan::escapeAlias(folder.alias));
    settings.setValue(QLatin1String("localPath"), folder.localPath);
    settings.setValue(QLatin1String("targetPath"), folder.targetPath);
    settings.setValue(QLatin1String("paused"), folder.paused);
    settings.setValue(QLatin1String("ignoreHiddenFiles"), folder.ignoreHiddenFiles);
    settings.endGroup();
}

bool FolderDefinition::load(QSettings& settings, const QString& alias,
                            FolderDefinition* folder)
{
    settings.beginGroup(alias);
    folder->alias = FolderMan::unescapeAlias(alias);
    folder->localPath = settings.value(QLatin1String("localPath")).toString();
    folder->targetPath = settings.value(QLatin1String("targetPath")).toString();
    folder->paused = settings.value(QLatin1String("paused")).toBool();
    folder->ignoreHiddenFiles = settings.value(QLatin1String("ignoreHiddenFiles"), QVariant(true)).toBool();
    settings.endGroup();

    // Old settings can contain paths with native separators. In the rest of the
    // code we assum /, so clean it up now.
    folder->localPath = prepareLocalPath(folder->localPath);

    return true;
}

QString FolderDefinition::prepareLocalPath(const QString& path)
{
    QString p = QDir::fromNativeSeparators(path);
    if (!p.endsWith(QLatin1Char('/'))) {
        p.append(QLatin1Char('/'));
    }
    return p;
}

} // namespace OCC

