/*
 * 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 "theme.h"
#include "filesystem.h"


#include "creds/abstractcredentials.h"

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

#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(AccountState* accountState,
               const QString& alias,
               const QString& path,
               const QString& secondPath,
               QObject* parent)
    : QObject(parent)
      , _accountState(accountState)
      , _path(path)
      , _remotePath(secondPath)
      , _alias(alias)
      , _paused(false)
      , _csyncError(false)
      , _csyncUnavail(false)
      , _wipeDb(false)
      , _proxyDirty(true)
      , _lastSyncDuration(0)
      , _forceSyncOnPollTimeout(false)
      , _consecutiveFailingSyncs(0)
      , _consecutiveFollowUpSyncs(0)
      , _journal(path)
      , _csync_ctx(0)
{
    qsrand(QTime::currentTime().msec());
    _timeSinceLastSyncStart.start();
    _timeSinceLastSyncDone.start();

    _syncResult.setStatus( SyncResult::NotYetStarted );

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

    _syncResult.setFolder(alias);
}

bool Folder::init()
{
    // We need to reconstruct the url because the path need 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 provlem 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( 11 );

        _accountState->account()->credentials()->syncContextPreInit(_csync_ctx);

        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);
}

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

void Folder::checkLocalPath()
{
    const QFileInfo fi(_path);

    if( fi.isDir() && fi.isReadable() ) {
        qDebug() << "Checked local path ok";
    } else {
        if( !FileSystem::fileExists(_path) ) {
            // try to create the local dir
            QDir d(_path);
            if( d.mkpath(_path) ) {
                qDebug() << "Successfully created the local dir " << _path;
            }
        }
        // Check directory again
        if( !FileSystem::fileExists(_path) ) {
            _syncResult.setErrorString(tr("Local folder %1 does not exist.").arg(_path));
            _syncResult.setStatus( SyncResult::SetupError );
        } else if( !fi.isDir() ) {
            _syncResult.setErrorString(tr("%1 should be a directory but is not.").arg(_path));
            _syncResult.setStatus( SyncResult::SetupError );
        } else if( !fi.isReadable() ) {
            _syncResult.setErrorString(tr("%1 is not readable.").arg(_path));
            _syncResult.setStatus( SyncResult::SetupError );
        }
    }
}

QString Folder::alias() const
{
    return _alias;
}

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

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

QString Folder::remotePath() const
{
    return _remotePath;
}

QUrl Folder::remoteUrl() const
{
    QUrl url = _accountState->account()->davUrl();
    QString path = url.path();
    if (!path.endsWith('/')) {
        path.append('/');
    }
    path.append(_remotePath);
    url.setPath(path);
    return url;
}

QString Folder::nativePath() const
{
    return QDir::toNativeSeparators(_path);
}

bool Folder::syncPaused() const
{
  return _paused;
}

void Folder::setSyncPaused( bool paused )
{
  _paused = paused;

  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" << alias() << "for changes via ETag check. (time since last sync:" << (_timeSinceLastSyncDone.elapsed() / 1000) << "s)";


    AccountPtr account = _accountState->account();

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

    if (_paused || !_accountState->isConnected()) {
        qDebug() << "Not syncing.  :"  << alias() << _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(alias());

    } 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) {
        _lastEtag = etag;
        emit scheduleToSync(alias());
    }
}

void Folder::etagRetreivedFromSyncEngine(const QString& etag)
{
    qDebug() << "Root etag from during sync:" << etag;
    _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;

    SyncFileItem firstItemNew;
    SyncFileItem firstItemDeleted;
    SyncFileItem firstItemUpdated;
    SyncFileItem firstItemRenamed;
    SyncFileItem firstItemError;

    SyncRunFileLog syncFileLog;

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

    QElapsedTimer timer;
    timer.start();

    foreach (const SyncFileItem &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.isEmpty()) {
                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:
                    newItems++;
                    if (firstItemNew.isEmpty())
                        firstItemNew = item;
                    break;
                case CSYNC_INSTRUCTION_REMOVE:
                    removedItems++;
                    if (firstItemDeleted.isEmpty())
                        firstItemDeleted = item;
                    break;
                case CSYNC_INSTRUCTION_CONFLICT:
                case CSYNC_INSTRUCTION_SYNC:
                    updatedItems++;
                    if (firstItemUpdated.isEmpty())
                        firstItemUpdated = item;
                    break;
                case CSYNC_INSTRUCTION_ERROR:
                    qDebug() << "Got Instruction ERROR. " << _syncResult.errorString();
                    break;
                case CSYNC_INSTRUCTION_RENAME:
                    if (firstItemRenamed.isEmpty()) {
                        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.isEmpty() ) {
        createGuiLog( firstItemNew._file,     SyncFileStatus::STATUS_NEW, newItems );
    }
    if( !firstItemDeleted.isEmpty() ) {
        createGuiLog( firstItemDeleted._file, SyncFileStatus::STATUS_REMOVE, removedItems );
    }
    if( ! firstItemUpdated.isEmpty() ) {
        createGuiLog( firstItemUpdated._file, SyncFileStatus::STATUS_UPDATED, updatedItems );
    }

    if( !firstItemRenamed.isEmpty() ) {
        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._file, 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(_path);
    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;
        QFile::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(alias());
        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
    // 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(alias());
    }
}

void Folder::setConfigFile( const QString& file )
{
    _configFile = file;
}

QString Folder::configFile()
{
    return _configFile;
}

static void addErroredSyncItemPathsToList(const SyncFileItemVector& items, QSet<QString>* set) {
    Q_FOREACH(const SyncFileItem &item, items) {
        if (item.hasErrorStatus()) {
            set->insert(item._file);
        }
    }
}

void Folder::slotThreadTreeWalkResult(const SyncFileItemVector& items)
{
    addErroredSyncItemPathsToList(items, &this->_stateLastSyncItemsWithError);
    _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->_stateLastSyncItemsWithError);
}


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::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();

    _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" );
}

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)
{
    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);
            return;
        }
        _clientProxy.setCSyncProxy(_accountState->account()->url(), _csync_ctx);
    } else if (proxyDirty()) {
        _clientProxy.setCSyncProxy(_accountState->account()->url(), _csync_ctx);
        setProxyDirty(false);
    }

    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 - client version"
             << qPrintable(Theme::instance()->version());

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

    _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()), SLOT(slotSyncFinished()), 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(Progress::Info)), this, SLOT(slotTransmissionProgress(Progress::Info)));
    connect(_engine.data(), SIGNAL(jobCompleted(SyncFileItem)), this, SLOT(slotJobCompleted(SyncFileItem)));
    connect(_engine.data(), SIGNAL(syncItemDiscovered(SyncFileItem)), this, SLOT(slotSyncItemDiscovered(SyncFileItem)));

    setDirtyNetworkLimits();
    _engine->setSelectiveSyncBlackList(selectiveSyncBlackList());

    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 = 0;
        if (cfg.useDownloadLimit()) {
            downloadLimit = cfg.downloadLimit() * 1000;
        }
        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::setSelectiveSyncBlackList(const QStringList& blackList)
{
    _selectiveSyncBlackList = blackList;
    for (int i = 0; i < _selectiveSyncBlackList.count(); ++i) {
        if (!_selectiveSyncBlackList.at(i).endsWith(QLatin1Char('/'))) {
            _selectiveSyncBlackList[i].append(QLatin1Char('/'));
        }
    }
}


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()
{
    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";
    }

    emit syncStateChange();

    // The syncFinished result that is to be triggered here makes the folderman
    // clearing 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)
{
    Progress::Info pi;
    pi._currentDiscoveredFolder = folderName;
    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 Progress::Info &pi)
{
    if( pi._completedFileCount ) {
        // No job completed yet, this is the beginning of a sync, set the warning level to 0
        _syncResult.setWarnCount(0);
    }
    ProgressDispatcher::instance()->setProgressInfo(alias(), pi);
}

// a job is completed: count the errors and forward to the ProgressDispatcher
void Folder::slotJobCompleted(const SyncFileItem &item)
{
    if (item.hasErrorStatus()) {
        _stateLastSyncItemsWithError.insert(item._file);
    }

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

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


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 file 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::ActionRole);
    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()));
    }
}
} // namespace OCC

