// Copyright (C) 2016 Alexander Drozdov.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "treescanner.h"

#include "projecttree.h"

#include <coreplugin/iversioncontrol.h>
#include <coreplugin/vcsmanager.h>

#include <utils/algorithm.h>
#include <utils/async.h>
#include <utils/hostosinfo.h>
#include <utils/qtcassert.h>
#include <utils/synchronizedvalue.h>

#include <qapplicationstatic.h>

#include <memory>

using namespace Utils;

namespace ProjectExplorer::TreeScanner {

bool isWellKnownBinary(const FilePath &fn)
{
    return fn.endsWith(QLatin1String(".a")) ||
           fn.endsWith(QLatin1String(".o")) ||
           fn.endsWith(QLatin1String(".d")) ||
           fn.endsWith(QLatin1String(".exe")) ||
           fn.endsWith(QLatin1String(".dll")) ||
           fn.endsWith(QLatin1String(".obj")) ||
           fn.endsWith(QLatin1String(".elf"));
}

using MimeBinaryCache = SynchronizedValue<QHash<QString, bool>>;
Q_APPLICATION_STATIC(MimeBinaryCache, s_mimeBinaryCache);

bool isMimeTypeIgnored(const MimeType &mimeType)
{
    if (auto it = s_mimeBinaryCache->get(
            [mimeType](const QHash<QString, bool> &cache) -> std::optional<bool> {
                auto cache_it = cache.find(mimeType.name());
                if (cache_it != cache.end())
                    return *cache_it;
                return std::nullopt;
            })) {
        return *it;
    }

    const bool isIgnored = !mimeType.inherits(QLatin1String("text/plain"));
    s_mimeBinaryCache->writeLocked()->insert(mimeType.name(), isIgnored);
    return isIgnored;
}

FileType genericFileType(const MimeType &mimeType)
{
    return Node::fileTypeForMimeType(mimeType);
}

struct DirectoryScanResult
{
    std::vector<std::unique_ptr<FileNode>> nodes;
    std::vector<std::unique_ptr<FolderNode>> subDirectories;
};

static DirectoryScanResult scanForFilesImpl(
    const QFuture<void> &future,
    const FilePath &directory,
    DirFilterFlags filter,
    const std::function<FileNode *(const FilePath &)> &factory,
    const QList<Core::IVersionControl *> &versionControls)
{
    DirectoryScanResult result;

    const FilePaths entries = directory.dirEntries(filter);
    for (const FilePath &entry : entries) {
        if (future.isCanceled())
            return result;

        if (Utils::anyOf(versionControls, [entry](const Core::IVersionControl *vc) {
                return vc->isVcsFileOrDirectory(entry);
            })) {
            continue;
        }

        if (entry.isDir())
            result.subDirectories.emplace_back(new FolderNode(entry));
        else if (FileNode *node = factory(entry))
            result.nodes.emplace_back(node);
    }
    return result;
}

static const MimeType &directoryMimeType()
{
    static const MimeType mimeType = Utils::mimeTypeForName("inode/directory");
    return mimeType;
}

static bool sortByPath(const std::unique_ptr<FileNode> &a, const std::unique_ptr<FileNode> &b)
{
    return a->filePath() < b->filePath();
}

static Result scanForFilesHelper(
    QPromise<Result> &promise,
    const FilePath &directory,
    DirFilterFlags dirfilter,
    const Filter &filter,
    const std::function<FileNode *(const FilePath &)> &factory)
{
    const QFuture<void> future(promise.future());

    const int progressRange = 1000000;
    const QList<Core::IVersionControl *> &versionControls = Core::VcsManager::versionControls();
    promise.setProgressRange(0, progressRange);

    QSet<FilePath> visited;
    DirectoryScanResult result = scanForFilesImpl(future, directory, dirfilter, factory,
                                                  versionControls);
    if (promise.isCanceled())
        return {};

    TreeScanner::Result finalResult;
    finalResult.allFiles = std::move(result.nodes);
    for (auto &fileNode : finalResult.allFiles)
        finalResult.firstLevelNodes.emplace_back(fileNode->clone());

    const int progressIncrement = int(
        progressRange / static_cast<double>(finalResult.allFiles.size() + result.subDirectories.size()));
    promise.setProgressValue(int(finalResult.allFiles.size() * progressIncrement));
    QList<QPair<FolderNode *, int>> subDirectories;
    auto addSubDirectories = [&](std::vector<std::unique_ptr<FolderNode>> &&subdirs, FolderNode * parent, int progressIncrement) {
        for (auto &subdir : subdirs) {
            if (Utils::insert(visited, subdir->filePath().canonicalPath())
                && !(filter && filter(directoryMimeType(), subdir->filePath()))) {
                subDirectories.append(qMakePair(subdir.get(), progressIncrement));
                subdir->setDisplayName(subdir->filePath().fileName());
                if (parent)
                    parent->addNode(std::move(subdir));
                else
                    finalResult.firstLevelNodes.emplace_back(std::move(subdir));
            } else {
                promise.setProgressValue(future.progressValue() + progressIncrement);
            }
        }
    };
    addSubDirectories(std::move(result.subDirectories), nullptr, progressIncrement);

    if (promise.isCanceled())
        return {};

    while (!subDirectories.isEmpty()) {
        using namespace QtTaskTree;
        const ListIterator iterator(subDirectories);
        subDirectories.clear();

        auto onSetup = [&, iterator](Async<DirectoryScanResult> &task) {
            task.setConcurrentCallData(
                scanForFilesImpl,
                future,
                iterator->first->filePath(),
                dirfilter,
                factory,
                versionControls);
        };

        auto onDone = [&, iterator](const Async<DirectoryScanResult> &task) {
            if (!task.isResultAvailable())
                return;
            const int progressRange = iterator->second;
            DirectoryScanResult result = task.takeResult();
            const qsizetype fileCount = result.nodes.size();
            for (auto &fileNode : result.nodes) {
                if (iterator->first)
                    iterator->first->addNode(std::unique_ptr<FileNode>(fileNode->clone()));
                finalResult.allFiles.emplace_back(std::move(fileNode));
            }
            const qsizetype subDirCount = result.subDirectories.size();
            if (subDirCount == 0) {
                promise.setProgressValue(future.progressValue() + progressRange);
            } else {
                const int increment = int(
                    progressRange / static_cast<double>(fileCount + subDirCount));
                promise.setProgressValue(future.progressValue() + increment * fileCount);
                addSubDirectories(std::move(result.subDirectories), iterator->first, increment);
            }
        };

        const Group recipe = For (iterator) >> Do {
            HostOsInfo::isLinuxHost() ? ParallelLimit(2) : parallelIdealThreadCountLimit,
            AsyncTask<DirectoryScanResult>(onSetup, onDone)
        };
        QTaskTree::runBlocking(recipe, future);

        if (promise.isCanceled())
            return {};
    }

    Utils::sort(finalResult.allFiles, sortByPath);
    return finalResult;
}

void scanForFiles(QPromise<Result> &promise,
                  const FilePath &directory,
                  const Filter &filter,
                  DirFilterFlags dirFilter,
                  const FileTypeFactory &factory)
{
    Result result = scanForFilesHelper(
        promise,
        directory,
        dirFilter,
        filter,
        [&filter, &factory](const FilePath &fn) -> FileNode * {
            const MimeType mimeType = Utils::mimeTypesForFileName(fn.path()).value(0);

            // Skip some files during scan.
            if (filter && filter(mimeType, fn))
                return nullptr;

            // Type detection
            FileType type = FileType::Unknown;
            if (factory)
                type = factory(mimeType);

            return new FileNode(fn, type);
        });

    promise.setProgressValue(promise.future().progressMaximum());
    promise.addResult(std::move(result));
}

} // namespace ProjectExplorer::TreeScanner
