/*
 *      fm-folder.c
 *
 *      Copyright 2009 - 2012 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
 *      Copyright 2012-2016 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
 *
 *      This file is a part of the Libfm library.
 *
 *      This library is free software; you can redistribute it and/or
 *      modify it under the terms of the GNU Lesser General Public
 *      License as published by the Free Software Foundation; either
 *      version 2.1 of the License, or (at your option) any later version.
 *
 *      This library 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
 *      Lesser General Public License for more details.
 *
 *      You should have received a copy of the GNU Lesser General Public
 *      License along with this library; if not, write to the Free Software
 *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "folder.h"
#include <string.h>
#include <cassert>
#include <QTimer>
#include <QDebug>

#include "dirlistjob.h"
#include "filesysteminfojob.h"
#include "fileinfojob.h"

namespace Fm {

std::unordered_map<FilePath, std::weak_ptr<Folder>, FilePathHash> Folder::cache_;
QString Folder::cutFilesDirPath_;
QString Folder::lastCutFilesDirPath_;
std::shared_ptr<const HashSet> Folder::cutFilesHashSet_;
std::mutex Folder::mutex_;

Folder::Folder():
    dirlist_job{nullptr},
    fsInfoJob_{nullptr},
    volumeManager_{VolumeManager::globalInstance()},
    /* for file monitor */
    has_idle_reload_handler{0},
    has_idle_update_handler{false},
    pending_change_notify{false},
    filesystem_info_pending{false},
    wants_incremental{false},
    stop_emission{false}, /* don't set it 1 bit to not lock other bits */
    /* filesystem info - set in query thread, read in main */
    fs_total_size{0},
    fs_free_size{0},
    has_fs_info{false},
    defer_content_test{false} {

    connect(volumeManager_.get(), &VolumeManager::mountAdded, this, &Folder::onMountAdded);
    connect(volumeManager_.get(), &VolumeManager::mountRemoved, this, &Folder::onMountRemoved);
}

Folder::Folder(const FilePath& path): Folder() {
    dirPath_ = path;
}

Folder::~Folder() {
    if(dirMonitor_) {
        g_signal_handlers_disconnect_by_data(dirMonitor_.get(), this);
        dirMonitor_.reset();
    }

    if(dirlist_job) {
        dirlist_job->cancel();
    }

    // cancel any file info job in progress.
    for(auto job: fileinfoJobs_) {
        job->cancel();
    }

    if(fsInfoJob_) {
        fsInfoJob_->cancel();
    }

    // We store a weak_ptr instead of shared_ptr in the hash table, so the hash table
    // does not own a reference to the folder. When the last reference to Folder is
    // freed, we need to remove its hash table entry.
    std::lock_guard<std::mutex> lock{mutex_};
    auto it = cache_.find(dirPath_);
    if(it != cache_.end()) {
        cache_.erase(it);
    }
}

// static
std::shared_ptr<Folder> Folder::fromPath(const FilePath& path) {
    std::lock_guard<std::mutex> lock{mutex_};
    auto it = cache_.find(path);
    if(it != cache_.end()) {
        auto folder = it->second.lock();
        if(folder) {
            return folder;
        }
        else { // FIXME: is this possible?
            cache_.erase(it);
        }
    }
    auto folder = std::make_shared<Folder>(path);
    folder->reload();
    cache_.emplace(path, folder);
    return folder;
}

bool Folder::makeDirectory(const char* /*name*/, GError** /*error*/) {
    // TODO:
    // FIXME: what the API is used for in the original libfm C API?
    return false;
}

bool Folder::isIncremental() const {
    return wants_incremental;
}

bool Folder::isValid() const {
    return dirInfo_ != nullptr;
}

bool Folder::isLoaded() const {
    return (dirlist_job == nullptr);
}

std::shared_ptr<const FileInfo> Folder::fileByName(const char* name) const {
    auto it = files_.find(name);
    if(it != files_.end()) {
        return it->second;
    }
    return nullptr;
}

bool Folder::isEmpty() const {
    return files_.empty();
}

FileInfoList Folder::files() const {
    FileInfoList ret;
    ret.reserve(files_.size());
    for(const auto& item : files_) {
        ret.push_back(item.second);
    }
    return ret;
}


const FilePath& Folder::path() const {
    auto pathStr = dirPath_.toString();
    // qDebug() << this << "FOLDER_PATH:" << pathStr.get() << dirPath_.gfile().get();
    //assert(!g_str_has_prefix(pathStr.get(), "file:"));
    return dirPath_;
}

const std::shared_ptr<const FileInfo>& Folder::info() const {
    return dirInfo_;
}

#if 0
void Folder::init(FmFolder* folder) {
    files = fm_file_info_list_new();
    G_LOCK(hash);
    if(G_UNLIKELY(hash_uses == 0)) {
        hash = g_hash_table_new((GHashFunc)fm_path_hash, (GEqualFunc)fm_path_equal);
        volume_monitor = g_volume_monitor_get();
        if(G_LIKELY(volume_monitor)) {
            g_signal_connect(volume_monitor, "mount-added", G_CALLBACK(on_mount_added), nullptr);
            g_signal_connect(volume_monitor, "mount-removed", G_CALLBACK(on_mount_removed), nullptr);
        }
    }
    hash_uses++;
    G_UNLOCK(hash);
}
#endif

void Folder::onIdleReload() {
    /* check if folder still exists */
    reload();
    // G_LOCK(query);
    has_idle_reload_handler = false;
    // G_UNLOCK(query);
}

void Folder::queueReload() {
    // G_LOCK(query);
    if(!has_idle_reload_handler) {
        has_idle_reload_handler = true;
        QTimer::singleShot(0, this, &Folder::onIdleReload);
    }
    // G_UNLOCK(query);
}

void Folder::onFileInfoFinished() {
    FileInfoJob* job = static_cast<FileInfoJob*>(sender());
    fileinfoJobs_.erase(std::find(fileinfoJobs_.cbegin(), fileinfoJobs_.cend(), job));

    if(job->isCancelled())
        return;

    FileInfoList files_to_add;
    FileInfoList files_to_delete;
    std::vector<FileInfoPair> files_to_update;

    const auto& paths = job->paths();
    const auto& deletionPaths = job->deletionPaths();
    const auto& infos = job->files();
    auto path_it = paths.cbegin();
    auto info_it = infos.cbegin();
    for(; path_it != paths.cend() && info_it != infos.cend(); ++path_it, ++info_it) {
        const auto& path = *path_it;
        const auto& info = *info_it;

        if(path == dirPath_) { // got the info for the folder itself.
            dirInfo_ = info;
        }
        // add/update the file only if it isn't going to be deleted
        else if(std::find(deletionPaths.cbegin(), deletionPaths.cend(), path) == deletionPaths.cend()) {
            auto it = files_.find(info->name());
            if(it != files_.end()) { // the file already exists, update
                files_to_update.push_back(std::make_pair(it->second, info));
            }
            else { // newly added
                files_to_add.push_back(info);
            }
            files_[info->name()] = info;
        }
    }
    if(!files_to_add.empty()) {
        Q_EMIT filesAdded(files_to_add);
    }
    if(!files_to_update.empty()) {
        Q_EMIT filesChanged(files_to_update);
    }
    // deletion should be done now, after the info job is processed
    for(const auto &path: deletionPaths) {
        auto name = path.baseName();
        auto it = files_.find(name.get());
        if(it != files_.end()) {
            files_to_delete.push_back(it->second);
            files_.erase(it);
        }
    }
    if(!files_to_delete.empty()) {
        Q_EMIT filesRemoved(files_to_delete);
    }
    Q_EMIT contentChanged();
}

void Folder::processPendingChanges() {
    has_idle_update_handler = false;
    // FmFileInfoJob* job = nullptr;
    std::lock_guard<std::mutex> lock{mutex_};

    // idle_handler = 0;
    /* if we were asked to block updates let delay it for now */
    if(stop_emission) {
        return;
    }

    FileInfoJob* info_job = nullptr;
    if(!paths_to_update.empty() || !paths_to_add.empty() || !paths_to_del.empty()) {
        FilePathList paths, deletionPaths;
        paths.insert(paths.end(), paths_to_add.cbegin(), paths_to_add.cend());
        paths.insert(paths.end(), paths_to_update.cbegin(), paths_to_update.cend());
        deletionPaths.insert(deletionPaths.end(), paths_to_del.cbegin(), paths_to_del.cend());
        info_job = new FileInfoJob{paths, deletionPaths, dirPath_,
                                   hasCutFiles() ? cutFilesHashSet_ : nullptr};
        paths_to_update.clear();
        paths_to_add.clear();
        paths_to_del.clear();
    }

    if(info_job) {
        fileinfoJobs_.push_back(info_job);
        info_job->setAutoDelete(true);
        connect(info_job, &FileInfoJob::finished, this, &Folder::onFileInfoFinished, Qt::BlockingQueuedConnection);
        info_job->runAsync();
#if 0
        pending_jobs = g_slist_prepend(pending_jobs, job);
        if(!fm_job_run_async(FM_JOB(job))) {
            pending_jobs = g_slist_remove(pending_jobs, job);
            g_object_unref(job);
            g_critical("failed to start folder update job");
        }
#endif
    }

    if(pending_change_notify) {
        Q_EMIT changed();
        /* update volume info */
        queryFilesystemInfo();
        pending_change_notify = false;
    }

    if(filesystem_info_pending) {
        Q_EMIT fileSystemChanged();
        filesystem_info_pending = false;
    }
}

/* should be called only with G_LOCK(lists) on! */
void Folder::queueUpdate() {
    // qDebug() << "queue_update:" << !has_idle_handler << paths_to_add.size() << paths_to_update.size() << paths_to_del.size();
    if(!has_idle_update_handler) {
        QTimer::singleShot(0, this, &Folder::processPendingChanges);
        has_idle_update_handler = true;
    }
}


/* returns true if reference was taken from path */
bool Folder::eventFileAdded(const FilePath &path) {
    bool added = true;
    // G_LOCK(lists);
    /* make sure that the file is not already queued for addition. */
    if(std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) {
        if(files_.find(path.baseName().get()) != files_.end()) { // the file already exists, update instead
            if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), path) == paths_to_update.cend()) {
                paths_to_update.push_back(path);
            }
        }
        else { // newly added file
            paths_to_add.push_back(path);
        }
        /* bug #3591771: 'ln -fns . test' leave no file visible in folder.
           If it is queued for deletion then cancel that operation */
        paths_to_del.erase(std::remove(paths_to_del.begin(), paths_to_del.end(), path), paths_to_del.cend());
    }
    else
        /* file already queued for adding, don't duplicate */
    {
        added = false;
    }
    if(added) {
        queueUpdate();
    }
    // G_UNLOCK(lists);
    return added;
}

bool Folder::eventFileChanged(const FilePath &path) {
    bool added;
    // G_LOCK(lists);
    /* make sure that the file is not already queued for changes, addition or deletion */
    if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), path) == paths_to_update.cend()
       && std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()
       && std::find(paths_to_del.cbegin(), paths_to_del.cend(), path) == paths_to_del.cend()) {
        /* Since this function is called only when a file already exists, even if that file
           isn't included in "files_" yet, it will be soon due to a previous call to queueUpdate().
           So, here, we should queue it for changes regardless of what "files_" may contain. */
        paths_to_update.push_back(path);
        added = true;
        queueUpdate();
    }
    else {
        added = false;
    }
    // G_UNLOCK(lists);
    return added;
}

void Folder::eventFileDeleted(const FilePath& path) {
    // qDebug() << "delete " << path.baseName().get();
    // G_LOCK(lists);
    /* Queue the file for deletion only if it is not in the addition queue but
       if it is, remove it from that queue instead of queueing it for deletion.
       Moreover, as was the case with eventFileChanged(), here too queueing
       should be done regardless of what "files_" may contain. */
    if(std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) {
        if(std::find(paths_to_del.cbegin(), paths_to_del.cend(), path) == paths_to_del.cend()) {
            paths_to_del.push_back(path);
        }
    }
    else {
        paths_to_add.erase(std::remove(paths_to_add.begin(), paths_to_add.end(), path), paths_to_add.cend());
    }
    /* the update queue should be canceled for a file that is going to be deleted */
    paths_to_update.erase(std::remove(paths_to_update.begin(), paths_to_update.end(), path), paths_to_update.cend());
    queueUpdate();
    // G_UNLOCK(lists);
}


void Folder::onDirChanged(GFileMonitorEvent evt) {
    switch(evt) {
    case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
        /* g_debug("folder is going to be unmounted"); */
        break;
    case G_FILE_MONITOR_EVENT_UNMOUNTED:
        Q_EMIT unmount();
        /* g_debug("folder is unmounted"); */
        queueReload();
        break;
    case G_FILE_MONITOR_EVENT_DELETED:
        Q_EMIT removed();
        /* g_debug("folder is deleted"); */
        break;
    case G_FILE_MONITOR_EVENT_CREATED:
        queueReload();
        break;
    case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
    case G_FILE_MONITOR_EVENT_CHANGED: {
        std::lock_guard<std::mutex> lock{mutex_};
        pending_change_notify = true;
        if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), dirPath_) != paths_to_update.cend()) {
            paths_to_update.push_back(dirPath_);
            queueUpdate();
        }
        /* g_debug("folder is changed"); */
        break;
    }
#if GLIB_CHECK_VERSION(2,24,0)
    case G_FILE_MONITOR_EVENT_MOVED:
#endif
    case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
        ;
    default:
        break;
    }
}

void Folder::onFileChangeEvents(GFileMonitor* /*monitor*/, GFile* gf, GFile* /*other_file*/, GFileMonitorEvent evt) {
    /* const char* names[]={
        "G_FILE_MONITOR_EVENT_CHANGED",
        "G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT",
        "G_FILE_MONITOR_EVENT_DELETED",
        "G_FILE_MONITOR_EVENT_CREATED",
        "G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED",
        "G_FILE_MONITOR_EVENT_PRE_UNMOUNT",
        "G_FILE_MONITOR_EVENT_UNMOUNTED"
    }; */
    if(dirPath_ == gf) {
        onDirChanged(evt);
        return;
    }
    else {
        std::lock_guard<std::mutex> lock{mutex_};
        auto path = FilePath{gf, true};
        /* NOTE: sometimes, for unknown reasons, GFileMonitor gives us the
         * same event of the same file for multiple times. So we need to
         * check for duplications ourselves here. */
        switch(evt) {
        case G_FILE_MONITOR_EVENT_CREATED:
            eventFileAdded(path);
            break;
        case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
        case G_FILE_MONITOR_EVENT_CHANGED:
            eventFileChanged(path);
            break;
        case G_FILE_MONITOR_EVENT_DELETED:
            eventFileDeleted(path);
            break;
        default:
            break;
        }
    }
}

// checks whether there were cut files here
// and if there were, invalidates this last cut path
bool Folder::hadCutFilesUnset() {
    if(lastCutFilesDirPath_ == dirPath_.toString().get()) {
        lastCutFilesDirPath_ = QString();
        return true;
    }
    return false;
}

bool Folder::hasCutFiles() {
    return cutFilesHashSet_
            && !cutFilesHashSet_->empty()
            && cutFilesDirPath_ == dirPath_.toString().get();
}

void Folder::setCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet) {
    if(cutFilesHashSet_ && !cutFilesHashSet_->empty()) {
        lastCutFilesDirPath_ = cutFilesDirPath_;
    }
    cutFilesDirPath_ = dirPath_.toString().get();
    cutFilesHashSet_ = cutFilesHashSet;
}

void Folder::onDirListFinished() {
    DirListJob* job = static_cast<DirListJob*>(sender());
    if(job->isCancelled()) { // this is a cancelled job, ignore!
        if(job == dirlist_job) {
            dirlist_job = nullptr;
        }
        Q_EMIT finishLoading();
        return;
    }
    dirInfo_ = job->dirInfo();

    FileInfoList files_to_add;
    std::vector<FileInfoPair> files_to_update;
    const auto& infos = job->files();

    // with "search://", there is no update for infos and all of them should be added
    if(strcmp(dirPath_.uriScheme().get(), "search") == 0) {
        files_to_add = infos;
        for(auto& file: files_to_add) {
            files_[file->name()] = file;
        }
    }
    else {
        auto info_it = infos.cbegin();
        for(; info_it != infos.cend(); ++info_it) {
            const auto& info = *info_it;
            auto it = files_.find(info->name());
            if(it != files_.end()) {
                files_to_update.push_back(std::make_pair(it->second, info));
            }
            else {
                files_to_add.push_back(info);
            }
            files_[info->name()] = info;
        }
    }

    if(!files_to_add.empty()) {
        Q_EMIT filesAdded(files_to_add);
    }
    if(!files_to_update.empty()) {
        Q_EMIT filesChanged(files_to_update);
    }

#if 0
    if(dirlist_job->isCancelled() && !wants_incremental) {
        GList* l;
        for(l = fm_file_info_list_peek_head_link(job->files); l; l = l->next) {
            FmFileInfo* inf = (FmFileInfo*)l->data;
            files = g_slist_prepend(files, inf);
            fm_file_info_list_push_tail(files, inf);
        }
        if(G_LIKELY(files)) {
            GSList* l;

            G_LOCK(lists);
            if(defer_content_test && fm_path_is_native(dir_path))
                /* we got only basic info on content, schedule update it now */
                for(l = files; l; l = l->next)
                    files_to_update = g_slist_prepend(files_to_update,
                                              fm_path_ref(fm_file_info_get_path(l->data)));
            G_UNLOCK(lists);
            g_signal_emit(folder, signals[FILES_ADDED], 0, files);
            g_slist_free(files);
        }

        if(job->dir_fi) {
            dir_fi = fm_file_info_ref(job->dir_fi);
        }

        /* Some new files are created while FmDirListJob is loading the folder. */
        G_LOCK(lists);
        if(G_UNLIKELY(files_to_add)) {
            /* This should be a very rare case. Could this happen? */
            GSList* l;
            for(l = files_to_add; l;) {
                FmPath* path = l->data;
                GSList* next = l->next;
                if(_Folder::get_file_by_path(folder, path)) {
                    /* we already have the file. remove it from files_to_add,
                     * and put it in files_to_update instead.
                     * No strdup for name is needed here. We steal
                     * the string from files_to_add.*/
                    files_to_update = g_slist_prepend(files_to_update, path);
                    files_to_add = g_slist_delete_link(files_to_add, l);
                }
                l = next;
            }
        }
        G_UNLOCK(lists);
    }
    else if(!dir_fi && job->dir_fi)
        /* we may need dir_fi for incremental folders too */
    {
        dir_fi = fm_file_info_ref(job->dir_fi);
    }
    g_object_unref(dirlist_job);
#endif

    dirlist_job = nullptr;
    Q_EMIT finishLoading();
}

#if 0


void on_dirlist_job_files_found(FmDirListJob* job, GSList* files, gpointer user_data) {
    FmFolder* folder = FM_FOLDER(user_data);
    GSList* l;
    for(l = files; l; l = l->next) {
        FmFileInfo* file = FM_FILE_INFO(l->data);
        fm_file_info_list_push_tail(files, file);
    }
    if(G_UNLIKELY(!dir_fi && job->dir_fi))
        /* we may want info while folder is still loading */
    {
        dir_fi = fm_file_info_ref(job->dir_fi);
    }
    g_signal_emit(folder, signals[FILES_ADDED], 0, files);
}

ErrorAction on_dirlist_job_error(FmDirListJob* job, GError* err, FmJobErrorSeverity severity, FmFolder* folder) {
    guint ret;
    /* it's possible that some signal handlers tries to free the folder
     * when errors occurs, so let's g_object_ref here. */
    g_object_ref(folder);
    g_signal_emit(folder, signals[ERROR], 0, err, (guint)severity, &ret);
    g_object_unref(folder);
    return ret;
}

void free_dirlist_job(FmFolder* folder) {
    if(wants_incremental) {
        g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_files_found, folder);
    }
    g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_finished, folder);
    g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_error, folder);
    fm_job_cancel(FM_JOB(dirlist_job));
    g_object_unref(dirlist_job);
    dirlist_job = nullptr;
}

#endif


void Folder::reload() {
    // cancel in-progress jobs if there are any
    GError* err = nullptr;
    // cancel directory monitoring
    if(dirMonitor_) {
        g_signal_handlers_disconnect_by_data(dirMonitor_.get(), this);
        dirMonitor_.reset();
    }

    /* clear all update-lists now, see SF bug #919 - if update comes before
       listing job is finished, a duplicate may be created in the folder */
    if(has_idle_update_handler) {
        // FIXME: cancel the idle handler
        paths_to_add.clear();
        paths_to_update.clear();
        paths_to_del.clear();

        // cancel any file info job in progress.
        for(auto job: fileinfoJobs_) {
            job->cancel();
            disconnect(job, &FileInfoJob::finished, this, &Folder::onFileInfoFinished);
        }
        fileinfoJobs_.clear();
    }

    /* remove all existing files */
    if(!files_.empty()) {
        // FIXME: this is not very efficient :(
        auto tmp = files();
        files_.clear();
        Q_EMIT filesRemoved(tmp);
    }

    /* Tell the world that we're about to reload the folder.
     * It might be a good idea for users of the folder to disconnect
     * from the folder temporarily and reconnect to it again after
     * the folder complete the loading. This might reduce some
     * unnecessary signal handling and UI updates. */
    Q_EMIT startLoading();

    dirInfo_.reset(); // clear dir info

    /* also re-create a new file monitor */
    // mon = GFileMonitorPtr{fm_monitor_directory(dir_path.gfile().get(), &err), false};
    // FIXME: should we make this cancellable?
    dirMonitor_ = GFileMonitorPtr{
            g_file_monitor_directory(dirPath_.gfile().get(), G_FILE_MONITOR_WATCH_MOUNTS, nullptr, &err),
            false
    };

    if(dirMonitor_) {
        g_signal_connect(dirMonitor_.get(), "changed", G_CALLBACK(_onFileChangeEvents), this);
    }
    else {
        qDebug("file monitor cannot be created: %s", err->message);
        g_error_free(err);
    }

    Q_EMIT contentChanged();

    /* run a new dir listing job */
    // FIXME:
    // defer_content_test = fm_config->defer_content_test;
    dirlist_job = new DirListJob(dirPath_, defer_content_test ? DirListJob::FAST : DirListJob::DETAILED,
                                 hasCutFiles() ? cutFilesHashSet_ : nullptr);
    dirlist_job->setAutoDelete(true);
    connect(dirlist_job, &DirListJob::error, this, &Folder::error, Qt::BlockingQueuedConnection);
    connect(dirlist_job, &DirListJob::finished, this, &Folder::onDirListFinished, Qt::BlockingQueuedConnection);

#if 0
    if(wants_incremental) {
        g_signal_connect(dirlist_job, "files-found", G_CALLBACK(on_dirlist_job_files_found), folder);
    }
    fm_dir_list_job_set_incremental(dirlist_job, wants_incremental);
#endif

    dirlist_job->runAsync();

    /* also reload filesystem info.
     * FIXME: is this needed? */
    queryFilesystemInfo();
}

#if 0

/**
 * Folder::is_incremental
 * @folder: folder to test
 *
 * Checks if a folder is incrementally loaded.
 * After an FmFolder object is obtained from calling Folder::from_path(),
 * if it's not yet loaded, it begins loading the content of the folder
 * and emits "start-loading" signal. Most of the time, the info of the
 * files in the folder becomes available only after the folder is fully
 * loaded. That means, after the "finish-loading" signal is emitted.
 * Before the loading is finished, Folder::get_files() returns nothing.
 * You can tell if a folder is still being loaded with Folder::is_loaded().
 *
 * However, for some special FmFolder types, such as the ones handling
 * search:// URIs, we want to access the file infos while the folder is
 * still being loaded (the search is still ongoing).
 * The content of the folder grows incrementally and Folder::get_files()
 * returns files currently being loaded even when the folder is not
 * fully loaded. This is what we called incremental.
 * Folder::is_incremental() tells you if the FmFolder has this feature.
 *
 * Returns: %true if @folder is incrementally loaded
 *
 * Since: 1.0.2
 */
bool Folder::is_incremental(FmFolder* folder) {
    return wants_incremental;
}

#endif

bool Folder::getFilesystemInfo(uint64_t* total_size, uint64_t* free_size) const {
    if(has_fs_info) {
        *total_size = fs_total_size;
        *free_size = fs_free_size;
        return true;
    }
    return false;
}


void Folder::onFileSystemInfoFinished() {
    FileSystemInfoJob* job = static_cast<FileSystemInfoJob*>(sender());
    if(job->isCancelled() || job != fsInfoJob_) { // this is a cancelled job, ignore!
        fsInfoJob_ = nullptr;
        has_fs_info = false;
        return;
    }
    has_fs_info = job->isAvailable();
    fs_total_size = job->size();
    fs_free_size = job->freeSize();
    filesystem_info_pending = true;
    fsInfoJob_ = nullptr;
    queueUpdate();
}


void Folder::queryFilesystemInfo() {
    // G_LOCK(query);
    if(fsInfoJob_)
        return;
    fsInfoJob_ = new FileSystemInfoJob{dirPath_};
    fsInfoJob_->setAutoDelete(true);
    connect(fsInfoJob_, &FileSystemInfoJob::finished, this, &Folder::onFileSystemInfoFinished, Qt::BlockingQueuedConnection);

    fsInfoJob_->runAsync();
    // G_UNLOCK(query);
}


#if 0
/**
 * Folder::block_updates
 * @folder: folder to apply
 *
 * Blocks emitting signals for changes in folder, i.e. if some file was
 * added, changed, or removed in folder after this API, no signal will be
 * sent until next call to Folder::unblock_updates().
 *
 * Since: 1.2.0
 */
void Folder::block_updates(FmFolder* folder) {
    /* g_debug("Folder::block_updates %p", folder); */
    G_LOCK(lists);
    /* just set the flag */
    stop_emission = true;
    G_UNLOCK(lists);
}

/**
 * Folder::unblock_updates
 * @folder: folder to apply
 *
 * Unblocks emitting signals for changes in folder. If some changes were
 * in folder after previous call to Folder::block_updates() then these
 * changes will be sent after this call.
 *
 * Since: 1.2.0
 */
void Folder::unblock_updates(FmFolder* folder) {
    /* g_debug("Folder::unblock_updates %p", folder); */
    G_LOCK(lists);
    stop_emission = false;
    /* query update now */
    queue_update(folder);
    G_UNLOCK(lists);
    /* g_debug("Folder::unblock_updates OK"); */
}

/**
 * Folder::make_directory
 * @folder: folder to apply
 * @name: display name for new directory
 * @error: (allow-none) (out): location to save error
 *
 * Creates new directory in given @folder.
 *
 * Returns: %true in case of success.
 *
 * Since: 1.2.0
 */
bool Folder::make_directory(FmFolder* folder, const char* name, GError** error) {
    GFile* dir, *gf;
    FmPath* path;
    bool ok;

    dir = fm_path_to_gfile(dir_path);
    gf = g_file_get_child_for_display_name(dir, name, error);
    g_object_unref(dir);
    if(gf == nullptr) {
        return false;
    }
    ok = g_file_make_directory(gf, nullptr, error);
    if(ok) {
        path = fm_path_new_for_gfile(gf);
        if(!_Folder::event_file_added(folder, path)) {
            fm_path_unref(path);
        }
    }
    g_object_unref(gf);
    return ok;
}

void Folder::content_changed(FmFolder* folder) {
    if(has_fs_info && !fs_info_not_avail) {
        Folder::query_filesystem_info(folder);
    }
}

#endif

/* NOTE:
 * GFileMonitor has some significant limitations:
 * 1. Currently it can correctly emit unmounted event for a directory.
 * 2. After a directory is unmounted, its content changes.
 *    Inotify does not fire events for this so a forced reload is needed.
 * 3. If a folder is empty, and later a filesystem is mounted to the
 *    folder, its content should reflect the content of the newly mounted
 *    filesystem. However, GFileMonitor and inotify do not emit events
 *    for this case. A forced reload might be needed for this case as well.
 * 4. Some limitations come from Linux/inotify. If FAM/gamin is used,
 *    the condition may be different. More testing is needed.
 */
void Folder::onMountAdded(const Mount& mnt) {
    /* If a filesystem is mounted over an existing folder,
     * we need to refresh the content of the folder to reflect
     * the changes. Besides, we need to create a new GFileMonitor
     * for the newly-mounted filesystem as the inode already changed.
     * GFileMonitor cannot detect this kind of changes caused by mounting.
     * So let's do it ourselves. */
    auto mountRoot = mnt.root();
    if(mountRoot.isPrefixOf(dirPath_)) {
        queueReload();
    }
    /* g_debug("FmFolder::mount_added"); */
}

void Folder::onMountRemoved(const Mount& mnt) {
    /* g_debug("FmFolder::mount_removed"); */

    /* NOTE: gvfs does not emit unmount signals for remote folders since
     * GFileMonitor does not support remote filesystems at all.
     * So here is the side effect, no unmount notifications.
     * We need to generate the signal ourselves. */
    if(!dirMonitor_) {
        // this is only needed when we don't have a GFileMonitor
        auto mountRoot = mnt.root();
        if(mountRoot.isPrefixOf(dirPath_)) {
            // if the current folder is under the unmounted path, generate the event ourselves
            onDirChanged(G_FILE_MONITOR_EVENT_UNMOUNTED);
        }
    }
}

} // namespace Fm
