/*
    SPDX-FileCopyrightText: 2013 Kåre Särs <kare.sars@iki.fi>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "FolderFilesList.h"

#include <QDir>
#include <QElapsedTimer>
#include <QFileInfoList>
#include <QtConcurrentMap>

#include <unordered_set>
#include <vector>

using namespace Qt::Literals::StringLiterals;

FolderFilesList::FolderFilesList(QObject *parent)
    : QThread(parent)
{
    // ensure we have a proper thread name during e.g. perf profiling
    setObjectName(QStringLiteral("FolderFilesList"));
}

FolderFilesList::~FolderFilesList()
{
    m_cancelSearch = true;
    wait();
}

void FolderFilesList::run()
{
    m_files.clear();

    /**
     * iterative algorithm, in each round, we put in X directories to traverse
     * we will get as output X times: new directories + found files
     */
    std::vector<DirectoryWithResults> directoriesWithResults{
        DirectoryWithResults{.directory = m_folder, .newDirectories = QStringList(), .newFiles = QStringList()}};
    std::unordered_set<QString> directoryGuard{m_folder};
    QElapsedTimer time;
    time.start();
    while (!directoriesWithResults.empty()) {
        /**
         * all 100 ms => inform about progress
         */
        if (time.elapsed() > 100) {
            time.restart();
            Q_EMIT searching(directoriesWithResults[0].directory);
        }

        /**
         * map the stuff blocking, we are in a background thread, easiest way
         * this will just span ideal thread count many things while this thread more or less sleeps
         * we call the checkNextItem member function, that one must be careful to not do evil things ;=)
         */
        QtConcurrent::blockingMap(directoriesWithResults, [this](DirectoryWithResults &item) {
            checkNextItem(item);
        });

        /**
         * collect the results to create new worklist for next round
         */
        std::vector<DirectoryWithResults> nextRound;
        for (const auto &result : directoriesWithResults) {
            /**
             * one new item for the next round for each new directory
             */
            for (const auto &newDirectory : result.newDirectories) {
                if (directoryGuard.insert(newDirectory).second) {
                    nextRound.push_back(DirectoryWithResults{.directory = newDirectory, .newDirectories = QStringList(), .newFiles = QStringList()});
                }
            }

            /**
             * just append found files
             */
            m_files << result.newFiles;
        }

        /**
         * let's get next round going
         */
        directoriesWithResults = nextRound;
    }

    if (m_cancelSearch) {
        m_files.clear();
    }
    Q_EMIT fileListReady();
}

void FolderFilesList::generateList(const QString &folder, bool recursive, bool hidden, bool symlinks, const QString &types, const QString &excludes)
{
    m_cancelSearch = false;
    m_folder = folder;
    if (!m_folder.endsWith(u'/')) {
        m_folder += u'/';
    }
    m_recursive = recursive;
    m_hidden = hidden;
    m_symlinks = symlinks;

    m_types.clear();
    const auto typesList = types.split(u',', Qt::SkipEmptyParts);
    for (const QString &type : typesList) {
        m_types << type.trimmed();
    }
    if (m_types.isEmpty()) {
        m_types << QStringLiteral("*");
    }

    const QStringList tmpExcludes = excludes.split(u',');
    m_excludes.clear();
    m_pathExcludes.clear();
    for (const auto &excl : tmpExcludes) {
        if (excl.contains('/'_L1)) {
            // If the exclude includes a '/', we match the whole path not just the path section
            m_pathExcludes << QRegularExpression(
                QRegularExpression::wildcardToRegularExpression(excl.trimmed(), QRegularExpression::UnanchoredWildcardConversion));
        } else {
            m_excludes << QRegularExpression(QRegularExpression::wildcardToRegularExpression(excl.trimmed()));
        }
    }
    start();
}

void FolderFilesList::terminateSearch()
{
    m_cancelSearch = true;
    wait();
    m_files.clear();
}

QStringList FolderFilesList::fileList()
{
    if (m_cancelSearch) {
        m_files.clear();
    }
    return m_files;
}

void FolderFilesList::checkNextItem(DirectoryWithResults &handleOnFolder) const
{
    /**
     * IMPORTANT: this member function is called by MULTIPLE THREADS
     * => it is const, it shall only modify handleOnFolder
     */

    if (m_cancelSearch) {
        return;
    }

    QDir currentDir(handleOnFolder.directory);
    if (!currentDir.isReadable()) {
        // qDebug() << currentDir.absolutePath() << "Not readable";
        return;
    }

    QDir::Filters filter = QDir::Files | QDir::NoDotAndDotDot | QDir::Readable;
    if (m_hidden) {
        filter |= QDir::Hidden;
    }
    if (m_recursive) {
        filter |= QDir::AllDirs;
    }
    if (!m_symlinks) {
        filter |= QDir::NoSymLinks;
    }

    const QFileInfoList entries = currentDir.entryInfoList(m_types, filter, QDir::Name | QDir::LocaleAware);
    for (const auto &entry : entries) {
        const QString absFilePath = entry.absoluteFilePath();
        bool skip{false};
        const QStringList pathSplit = absFilePath.split(u'/', Qt::SkipEmptyParts);
        for (const auto &regex : m_pathExcludes) {
            QRegularExpressionMatch match = regex.match(absFilePath);
            if (match.hasMatch()) {
                skip = true;
                break;
            }
        }
        for (const auto &regex : m_excludes) {
            for (const auto &part : pathSplit) {
                QRegularExpressionMatch match = regex.match(part);
                if (match.hasMatch()) {
                    skip = true;
                    break;
                }
            }
        }
        if (skip) {
            continue;
        }

        if (entry.isDir()) {
            handleOnFolder.newDirectories.append(absFilePath);
        }

        if (entry.isFile()) {
            handleOnFolder.newFiles.append(absFilePath);
        }
    }
}

#include "moc_FolderFilesList.cpp"
