/*
 * 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.
 */

#pragma once

#include "gui/owncloudguilib.h"

#include "accountstate.h"
#include "common/syncjournaldb.h"
#include "libsync/graphapi/space.h"
#include "networkjobs.h"
#include "progressdispatcher.h"
#include "syncoptions.h"
#include "syncresult.h"

#include <QDateTime>
#include <QObject>
#include <QUuid>
#include <QtQml/QtQml>

#include <chrono>
#include <memory>

class QThread;
class QSettings;

namespace OCC {

class Vfs;
class SyncEngine;
class SyncRunFileLog;
class FolderWatcher;
class LocalDiscoveryTracker;

/**
 * @brief The FolderDefinition class
 * @ingroup gui
 */
class OWNCLOUDGUI_EXPORT FolderDefinition
{
public:
    // use for reading from config
    FolderDefinition(const QByteArray &id, const QUrl &davUrl, const QString &spaceId, const QString &displayName);

    // use when creating folder from scratch for the very first time
    FolderDefinition(const QUrl &davUrl, const QString &spaceId, const QString &displayName = {});


    /// Saves the folder definition into the given settings (group should be preconfigured)
    static void save(QSettings &settings, const FolderDefinition &folder);

    /// Reads a folder definition from the current settings group.
    static FolderDefinition load(QSettings &settings, const QByteArray &id);

    /// path to the journal, usually relative to localPath
    QString journalPath() const { return _journalPath; }

    void setJournalPath(const QString &newPath)
    {
        if (_journalPath != newPath)
            _journalPath = newPath;
    }

    /// whether the folder is paused
    bool paused() const { return _paused; }
    void setPaused(bool pause) { _paused = pause; }

    /// Which virtual files setting the folder uses
    Vfs::Mode virtualFilesMode() const { return _virtualFilesMode; }
    void setVirtualFilesMode(Vfs::Mode mode) { _virtualFilesMode = mode; }

    /// Ensure / as separator and trailing /.
    void setLocalPath(const QString &path);
    QString localPath() const { return _localPath; }


    /// Remove ending /, then ensure starting '/': so "/foo/bar" and "/".
    void setTargetPath(const QString &path);
    QString targetPath() const { return _targetPath; }

    /// journalPath relative to localPath.
    QString absoluteJournalPath() const { return QDir(localPath()).filePath(_journalPath); }


    QUrl webDavUrl() const { return _webDavUrl; }
    // could change in the case of spaces
    void setWebDavUrl(const QUrl &url) { _webDavUrl = url; }


    // when using spaces we don't store the dav URL but the space id
    // this id is then used to look up the dav URL
    QString spaceId() const { return _spaceId; }
    void setSpaceId(const QString &spaceId) { _spaceId = spaceId; }

    const QByteArray &id() const { return _id; }

    QString displayName() const;

    /**
     * The folder is deployed by an admin
     * We will hide the remove option and the disable/enable vfs option.
     */
    // todo #52 Release 8.0 = eliminate this OC10 property and remove prop from settings
    [[deprecated("deployed concept is no longer supported and will be removed in client 8.0")]] bool isDeployed() const { return _deployed; }

    /**
     * Higher values mean more important
     * Used for sorting
     */
    uint32_t priority() const;
    void setPriority(uint32_t newPriority);

private:
    /// path to the journal, usually relative to localPath
    QString _journalPath;

    /// whether the folder is paused
    bool _paused = false;
    /// Which virtual files setting the folder uses
    Vfs::Mode _virtualFilesMode = Vfs::Off;

    // oc10 and as cache for ocis
    QUrl _webDavUrl;

    QString _spaceId;
    /// For legacy reasons this can be a string, new folder objects will use an uuid
    QByteArray _id;
    QString _displayName;
    /// path on local machine (always trailing /)
    QString _localPath;
    /// path on remote (usually no trailing /, exception "/")
    QString _targetPath;
    bool _deployed = false;

    uint32_t _priority = 0;

};

/**
 * @brief The Folder class
 * @ingroup gui
 */
// Refactoring todo: this class is overgrown and has too many responsibilities. Need to evaluate and refine it's core behaviors
// and split out some of the responsibilities to new dedicated classes. Eg I already see some need for a folder sync handler, and/or
// a folder scoped vfs controller. these could still be created/configured in the Folder (worst case!) but this class just does way too much
// Refactoring the impls to be more modular will also allow us to refactor the ctr sensibly to avoid the excessive setup routines which are
// invoked before the class is even fully built.
// we also have weird split responsibility around instantiating the vfs - on creation the FolderMan instantiates vfs and passes it to
// the ctr. But the folder resets the vfs with new instance sometimes as well. This is not healthy as responsibility is split/not clear
class OWNCLOUDGUI_EXPORT Folder : public QObject
{
    Q_OBJECT
    Q_PROPERTY(GraphApi::Space *space READ space NOTIFY spaceChanged)
    QML_ELEMENT
    QML_UNCREATABLE("Folders can only be created by the FolderManager")

public:
    enum class ChangeReason {
        Other,
        UnLock
    };
    Q_ENUM(ChangeReason)

    static void prepareFolder(const QString &path);

    /** Create a new Folder
     */
    Folder(const FolderDefinition &definition, AccountState *accountState, std::unique_ptr<Vfs> &&vfs, bool ignoreHiddenFiles, QObject *parent = nullptr);

    ~Folder() override;
    /**
     * The account the folder is configured on.
     */
    AccountState *accountState() const { return _accountState; }

    const FolderDefinition &definition() const { return _definition; }

    /**
     * id is the id used to identify a folder/space in the config file
     *
     * this id is "legacy" from the days when not all supported servers (eg oc10) supported spaces. Ideally we should remove this in favor of always using
     the space id but it would require changes to the config.
    */
    QByteArray id() const { return _definition.id(); }

    /**
     * @brief spaceId is the primary identifier for a folder, and it is guaranteed to be unique *within an account*.
     *
     * It is not universally unique because "shares" space has a hard coded spaceId which is always used regardless of where it lives.
     *
     * @return the space id for the folder
     */
    QString spaceId() const { return _definition.spaceId(); }

    /**
     * remote folder path, usually without trailing /, exception "/"
     */
    QString remotePath() const { return _definition.targetPath(); }

    // Normally this value comes from the space, but may come from the definition when the space is not available
    QString displayName() const;

    // Normally this value comes from the space, but may come from the definition when the space is not available
    QUrl webDavUrl() const;

    /**
     * short local path to display on the GUI  (native separators)
     */
    QString shortGuiLocalPath() const;

    /**
     * canonical local folder path, always ends with /
     */
    QString path() const { return _canonicalLocalPath; }

    /**
     * cleaned canonical folder path, like path() but never ends with a /
     *
     * Wrapper for QDir::cleanPath(path()) except for "Z:/",
     * where it returns "Z:" instead of "Z:/".
     */
    QString cleanPath() const;


    /**
     * remote folder path, always with a trailing /
     */
    QString remotePathTrailingSlash() const;

    /**
     * remote folder path with server URL
     */
    QUrl remoteUrl() const { return Utility::concatUrlPath(webDavUrl(), remotePath()); }

    /**
     * switch sync on or off
     */
    void setSyncPaused(bool);

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

    /** a folder is unavailable if its space is missing from the spaces manager
     *  this happens when a folder has been added for an existing space, then that space is disabled
     *  or deleted on the server.
     *  we should not try to sync unavailable spaces!
     */
    bool isAvailable() const { return _available; }
    void setAvailable(bool available);

    // is the folder connected (via the accountState)
    bool isConnected();
    /**
     * Returns true when the folder may sync.
     */
    bool canSync() const;

    /**
     * Whether the folder is ready
     *
     * passthrough to _engine
     */
    bool isReady() const;

    bool hasSetupError() const
    {
        return _syncResult.status() == SyncResult::SetupError;
    }

    void prepareToSync() { setSyncState(SyncResult::NotYetStarted); }

    /** True if the folder is currently synchronizing */
    bool isSyncRunning() const;

    /**
     * return the last sync result with error message and status
     */
    SyncResult syncResult() const { return _syncResult; }

    /**
      * This is called when the sync folder definition is removed. Do cleanups here.
      *
      * It removes the database, among other things.
      *
      * The folder is not in a valid state afterwards!
      */
    virtual void wipeForRemoval();

    void setSyncState(SyncResult::Status state);

    void reloadSyncOptions();


    // TODO: don't expose
    SyncJournalDb *journalDb()
    {
        return &_journal;
    }
    // TODO: don't expose
    SyncEngine &syncEngine()
    {
        return *_engine;
    }

    Vfs &vfs()
    {
        OC_ENFORCE(_vfs);
        return *_vfs;
    }

    auto lastSyncTime() const { return QDateTime::currentDateTime().addMSecs(-msecSinceLastSync().count()); }
    std::chrono::milliseconds msecSinceLastSync() const { return std::chrono::milliseconds(_timeSinceLastSyncDone.elapsed()); }
    std::chrono::milliseconds msecLastSyncDuration() const { return _lastSyncDuration; }

    /**
      * Returns whether a file inside this folder should be excluded.
      */
    bool isFileExcludedAbsolute(const QString &fullPath) const;

    /**
      * Returns whether a file inside this folder should be excluded.
      */
    bool isFileExcludedRelative(const QString &relativePath) const;

    /** virtual files of some kind are enabled
     *
     * This is independent of whether new files will be virtual. It's possible to have this enabled
     * and never have an automatic virtual file. But when it's on, the shell context menu will allow
     * users to make existing files virtual.
     */
    bool virtualFilesEnabled() const;
    void setVirtualFilesEnabled(bool enabled);

    /**
     * The folder is deployed by an admin
     * We will hide the remove option and the disable/enable vfs option.
     *
     * see FolderDefinition isDeployed - this concept was oc10 related so it needs to go
     */
    [[deprecated("see FolderDefinition::isDeployed")]] bool isDeployed() const;

    uint32_t sortPriority() const { return _definition.priority(); }

    static Result<void, QString> checkPathLength(const QString &path);

    /**
     *
     * @return The corresponding space object or null
     */
    GraphApi::Space *space() const;

Q_SIGNALS:
    void syncStateChange();
    void syncStarted();
    void syncFinished(const SyncResult &result);
    void syncPausedChanged(Folder *, bool paused);
    void canSyncChanged();
    void spaceChanged();
    void vfsModeChanged(Folder *f, Vfs::Mode newMode);
    void displayNameChanged();
    void imageChanged(); // probably part of space changed
    void progressUpdate(const ProgressInfo &progress);


    /**
     * Fires for each change inside this folder that wasn't caused
     * by sync activity.
     */
    void watchedFileChangedExternally(const QString &path);

public Q_SLOTS:
    /**
       * terminate the current sync run
       */
    void slotTerminateSync(const QString &reason);

    /**
      * Starts a sync operation
      *
      * If the list of changed files is known, it is passed.
      */
    void startSync();

    void slotDiscardDownloadProgress();
    int slotWipeErrorBlacklist();

    /**
       * Triggered by the folder watcher when a file/dir in this folder
       * changes. Needs to check whether this change should trigger a new
       * sync run to be scheduled.
       */
    void slotWatchedPathsChanged(const QSet<QString> &paths, ChangeReason reason);

    /**
     * Mark a virtual file as being requested for download, and start a sync.
     *
     * "implicit" here means that this download request comes from the user wanting
     * to access the file's data. The user did not change the file's pin state.
     * If the file is currently OnlineOnly its state will change to Unspecified.
     *
     * The download request is stored by setting ItemTypeVirtualFileDownload
     * in the database. This is necessary since the hydration is not driven by
     * the pin state.
     *
     * relativepath is the folder-relative path to the file (including the extension)
     *
     * Note, passing directories is not supported. Files only.
     */
    void implicitlyHydrateFile(const QString &relativepath);

    /** Ensures that the next sync performs a full local discovery. */
    void slotNextSyncFullLocalDiscovery();

    /** Adds the path to the local discovery list
     *
     * A weaker version of slotNextSyncFullLocalDiscovery() that just
     * schedules all parent and child items of the path for local
     * discovery.
     */
    void schedulePathForLocalDiscovery(const QString &relativePath);

    /// Reloads the excludes, used when changing the user-defined excludes after saving them to disk.
    bool reloadExcludes();

private Q_SLOTS:
    void slotSyncStarted();
    void slotSyncFinished(bool);

    /** Adds an error message that's not tied to a specific item.
     */
    void slotSyncError(const QString &message, ErrorCategory category = ErrorCategory::Normal);

    void slotItemCompleted(const SyncFileItemPtr &);

    void slotLogPropagationStart();

    /** Adjust sync result based on conflict data from SyncErrorWidget.
     *
     * This is pretty awkward, but SyncErrorWidget just keeps better track
     * of conflicts across partial local discovery.
     */
    void slotFolderConflicts(Folder *folder, const QStringList &conflictPaths);

    /** Warn users if they create a file or folder that is selective-sync excluded */
    void warnOnNewExcludedItem(const SyncJournalFileRecord &record, QStringView path);

    /** Warn users about an unreliable folder watcher */
    void slotWatcherUnreliable(const QString &message);

private:
    void showSyncResultPopup();

    bool checkLocalPath();

    SyncOptions loadSyncOptions();

    /**
     * Sets up this folder's folderWatcher if possible.
     *
     * May be called several times.
     */
    // Refactoring todo: I would expect a "registration" function to take an arg for the thing that is supposed to be registered
    // I also do not see evidence that this is called "several times" aside from the fact that it's called in relation to vfs::started()
    // so if vfs is in play, any time you turn vfs on or off for an existing folder, yes it can be called again. Under normal circumstances
    // I don't expect it to be very common for it to get called multiple times, though. Part of the todo is to evaluate whether it makes sense
    // to re/build the watcher relative to vfs state as I don't see any connection conceptually (yet, at least).
    void registerFolderWatcher();

    enum LogStatus {
        LogStatusRemove,
        LogStatusRename,
        LogStatusMove,
        LogStatusNew,
        LogStatusError,
        LogStatusConflict,
        LogStatusUpdated
    };

    void createGuiLog(const QString &filename, LogStatus status, int count,
        const QString &renameTarget = QString());

    void startVfs();

    void changeVfsMode(Vfs::Mode newMode);

    QPointer<AccountState> _accountState;
    FolderDefinition _definition;
    QString _canonicalLocalPath; // As returned with QFileInfo:canonicalFilePath.  Always ends with "/"

    SyncResult _syncResult;
    QScopedPointer<SyncEngine> _engine;
    QElapsedTimer _timeSinceLastSyncDone;
    QElapsedTimer _timeSinceLastSyncStart;
    QElapsedTimer _timeSinceLastFullLocalDiscovery;
    std::chrono::milliseconds _lastSyncDuration = {};

    /// The number of syncs that failed in a row.
    /// Reset when a sync is successful.
    int _consecutiveFailingSyncs = 0;

    /// The number of requested follow-up syncs.
    /// Reset when no follow-up is requested.
    int _consecutiveFollowUpSyncs = 0;

    mutable SyncJournalDb _journal;

    QScopedPointer<SyncRunFileLog> _fileLog;

    QTimer _scheduleSelfTimer;

    /**
     * Setting up vfs can be an async operation which is triggered by vfs::startImpl
     * this bool is set to true once vfs is fully started as notified via the started() signal
     * when a "real" vfs impl is in play, started() is emitted after the async setup is complete
     * when vfs is effectively "off", started() is emitted immediately by startImpl
     * main point is that even when vfs is not actually in play we pretend it's "started" -> _vfsIsReady will be true
     */
    bool _vfsIsReady = false;

    // does the folder have a corresponding space on the server? folderman will update the value based on info we get
    // from spaces manager.
    bool _available = true;

    /**
     * Watches this folder's local directory for changes.
     *
     * Created by registerFolderWatcher(), triggers slotWatchedPathsChanged()
     */
    // Refactor todo: I think this should just be a normal pointer since it's not passed around and is truly a child of the
    // parent Folder instance, so will be destructed along with the folder. Why are we making this so complicated?
    // Further, I'm not even sure it's safe to use a scoped pointer here since the whole purpose is to auto delete the pointer
    // when it goes "out of scope" - eg when this parent folder is destructed. If we have some real reason to keep it,
    // I think at the very least we need to be sure to use the QScopedPointerDeleteLater custom deleter as described in the docs:
    // QScopedPointer<MyCustomClass, ScopedPointerCustomDeleter> customPointer(new MyCustomClass);
    QScopedPointer<FolderWatcher> _folderWatcher;

    /**
     * Keeps track of locally dirty files so we can skip local discovery sometimes.
     */
    // Refactoring todo: as above, except we need to add the possibility to pass a parent to this class's ctr to allow auto-cleanup
    // again: this should NOT be complicated
    QScopedPointer<LocalDiscoveryTracker> _localDiscoveryTracker;

    /**
     * The vfs mode instance (created by plugin) to use. Never null. When vfs is not in play, vfs_off is the impl used.
     */
    // Refactoring todo: this is shared with the SyncOptions that are passed to the engine. This needs reevaluation and cleanup
    // to ensure we don't keep it alive outside of usable scope. Would probably be simplest to make it a QPointer for use with the
    // SyncOptions and rely on normal qt parenting scheme to ensure correct cleanup timing if it's not explicitly deleted.
    // it is also false that it is never null - it is reset in wipeForRemoval
    // extra fun is I have no idea what happens to the instance in the SyncOptions - is it still alive relative to the engine?
    // I don't see any handling of the engine or SyncOptions whatsoever in wipeForRemoval so we'll need to go spelunking.
    QSharedPointer<Vfs> _vfs;
};
}
