/*
 *
 * Copyright (C) 2023, KylinSoft Co., Ltd.
 *
 * 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 3 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Authors: iaom <zhangpengfei@kylinos.cn>
 */
#include "file-search-task.h"
#include <QDir>
#include <QUrl>
#include <QFile>
#include <QQueue>
#include <QDebug>
#include <QDateTime>
#include <gio/gio.h>
#include <QUrl>
#include <QTextBoundaryFinder>
#include "index-status-recorder.h"
#include "dir-watcher.h"
#include "common.h"
#include "file-utils.h"

using namespace UkuiSearch;
FileSearchTask::FileSearchTask(QObject *parent)
{
    this->setParent(parent);
    qRegisterMetaType<size_t>("size_t");
    m_pool = new QThreadPool(this);
    m_pool->setMaxThreadCount(1);
}

FileSearchTask::~FileSearchTask()
{
    m_pool->clear();
    m_pool->waitForDone();
}

void FileSearchTask::setController(const SearchController &searchController)
{
    m_searchController = searchController;
}

const QString FileSearchTask::name()
{
    return "File";
}

const QString FileSearchTask::description()
{
    return "File search.";
}

QString FileSearchTask::getCustomSearchType()
{
    return "File";
}

void FileSearchTask::startSearch()
{
    FileSearchWorker *fileSearchWorker;
    fileSearchWorker = new FileSearchWorker(this, &m_searchController);
    m_pool->start(fileSearchWorker);
}

void FileSearchTask::stop()
{

}

bool FileSearchTask::isSearching()
{
    return m_pool->activeThreadCount() > 0;
}

FileSearchWorker::FileSearchWorker(FileSearchTask *fileSarchTask, SearchController *searchController) : m_FileSearchTask(fileSarchTask)
{
    m_searchController = searchController;
    m_currentSearchId = m_searchController->getCurrentSearchId();
}

bool FileSearchWorker::underBlocked(const QString &path)
{
    for (const QString& skipDir : m_skipCheckDirs) {
        if (FileUtils::isOrUnder(path, skipDir)) {
            return false;
        }
    }

    QStringList blockList = DirWatcher::getDirWatcher()->getBlockDirsOfUser();
    for(const QString& i : blockList) {
        if(FileUtils::isOrUnder(path, i))
            return true;
    }
    return false;
}

bool isUnderBlackList(const QString& path)
{
    bool res(false);
    for (const QString &blackPath : DirWatcher::getDirWatcher()->currentBlackListOfIndex()) {
        if (FileUtils::isOrUnder(path, blackPath)) {
            res = true;
        }
    }
    return res;
}

void FileSearchWorker::run()
{
    //1.检查是否为不可搜索目录
    QStringList searchDirs = m_searchController->getSearchDir();
    searchDirs.removeDuplicates();

    for (QString &dir : searchDirs) {
        if (QFileInfo::exists(dir)) {
            if (dir.endsWith("/") && dir != "/") {
                dir.remove(dir.length() - 1, 1);
            }

            if (underBlocked(dir)) {
                m_skipCheckDirs.append(dir);
            }

            QStringList blackListTmp = DirWatcher::getDirWatcher()->blackListOfDir(dir);
            if (!blackListTmp.contains(dir)) {
                m_validDirectories.append(dir);
                m_blackList.append(blackListTmp);
            }
        }
    }

    //过滤空标签
    for (QString &label : m_searchController->getFileLabel()) {
        if (!label.isEmpty()) {
            m_labels.append(label);
        }
    }

    bool finished = true;
    bool indexed = true;

    QStringList indexedDir = DirWatcher::getDirWatcher()->currentIndexableDir();
    if(!indexedDir.isEmpty() && IndexStatusRecorder::getInstance()->indexDatabaseEnable() && !m_searchController->searchHiddenFiles()) {
        for(const QString &path : m_searchController->getSearchDir()) {
            bool pathIndexed = false;
            for(const QString &dir : indexedDir) {
                if(FileUtils::isOrUnder(path, dir) && (!isUnderBlackList(path) || isOrUnderExtraDir(path, false))) {
                    pathIndexed = true;
                    break;
                }
            }
            if(!pathIndexed) {
                indexed = false;
                break;
            }
        }
    } else {
        indexed = false;
    }

    //TODO 还需要判断是否为不能建立索引的目录
    if (indexed) {
        qDebug() << "index ready";
        finished = searchWithIndex();
    } else {
        if (m_validDirectories.empty()) {
            //TODO 使用全局的默认可搜索目录
            if (!QFileInfo::exists(QDir::homePath())) {
                sendErrorMsg(QObject::tr("Warning, Can not find home path."));
                return;
            }
            m_validDirectories.append(QDir::homePath());
            m_blackList.append(DirWatcher::getDirWatcher()->blackListOfDir(QDir::homePath()));
        }
        qDebug() << "direct search";
        finished = directSearch();
    }

    if (finished) QMetaObject::invokeMethod(m_FileSearchTask, "searchFinished", Q_ARG(size_t, m_currentSearchId));
}

Xapian::Query FileSearchWorker::creatQueryForFileSearch() {
    Xapian::Query fileOrDir = Xapian::Query::MatchAll;
    if (m_searchController->isSearchDirOnly() && m_searchController->isSearchFileOnly()) {
        //同时指定'只搜索目录'和'只搜索文件',那麼拒絕搜索
        return {};

    } else {
        if (m_searchController->isSearchDirOnly()) {
            fileOrDir = Xapian::Query(Xapian::Query::OP_VALUE_RANGE, 1, "1", "1");

        } else if (m_searchController->isSearchFileOnly()) {
            fileOrDir = Xapian::Query(Xapian::Query::OP_VALUE_RANGE, 1, "0", "0");
        }
    }

    std::vector<Xapian::Query> queries;
    if (m_searchController->getKeyword().isEmpty() && m_searchController->getMatchAllIfNoKeyword()) {
        queries.emplace_back(Xapian::Query::MatchAll);
    } else {
        for (QString &keyword : m_searchController->getKeyword()) {
            if (!keyword.isEmpty()) {
                std::vector<Xapian::Query> queryOfKeyword;
                QString inputKeyWord = keyword.toLower();
                QTextBoundaryFinder bf(QTextBoundaryFinder::Grapheme, inputKeyWord.toLower());
                int start = 0;
                for(; bf.position() != -1; bf.toNextBoundary()) {
                    int end = bf.position();
                    if(bf.boundaryReasons() & QTextBoundaryFinder::EndOfItem) {
                        queryOfKeyword.emplace_back(QUrl::toPercentEncoding(inputKeyWord.toLower().mid(start, end - start)).toStdString());
                    }
                    start = end;
                }
                queries.emplace_back(Xapian::Query::OP_PHRASE, queryOfKeyword.begin(), queryOfKeyword.end());
            }
        }
    }

    return {Xapian::Query::OP_AND, {Xapian::Query::OP_AND, queries.begin(), queries.end()}, fileOrDir};
}

bool FileSearchWorker::searchWithIndex()
{
    try {
        Xapian::Database db(INDEX_PATH.toStdString());
        Xapian::Enquire enquire(db);
//        qDebug() << "===" << QString::fromStdString(creatQueryForFileSearch().get_description());
        enquire.set_query(creatQueryForFileSearch());
        FileSearchFilter fileSearchFilter(this);

        Xapian::MSet result = enquire.get_mset(0, m_searchController->maxResults(), 0, &fileSearchFilter);

        for (auto it = result.begin(); it != result.end(); ++it) {
            if (m_searchController->beginSearchIdCheck(m_currentSearchId)) {
                QString path = QString::fromStdString(it.get_document().get_data());
                SearchResultProperties properties = m_searchController->getResultProperties(SearchProperty::SearchType::File);
                ResultItem resultItem(m_currentSearchId);
                resultItem.setItemKey(path);
                if(properties.contains(SearchProperty::SearchResultProperty::FilePath)) {
                    resultItem.setValue(SearchProperty::SearchResultProperty::FilePath, path);
                }
                if(properties.contains(SearchProperty::SearchResultProperty::FileIconName)) {
                    resultItem.setValue(SearchProperty::SearchResultProperty::FileIconName, FileUtils::getFileIcon(QUrl::fromLocalFile(path).toString()).name());
                }
                if(properties.contains(SearchProperty::SearchResultProperty::FileName)) {
                    resultItem.setValue(SearchProperty::SearchResultProperty::FileName, path.section("/", -1));
                }
                if(properties.contains(SearchProperty::SearchResultProperty::ModifiedTime)) {
                    resultItem.setValue(SearchProperty::SearchResultProperty::ModifiedTime,
                                        QDateTime::fromString(QString::fromStdString(it.get_document().get_value(2)), "yyyyMMddHHmmsszzz"));
                }
                m_searchController->getDataQueue()->enqueue(resultItem);
                if(++m_resultNum == m_searchController->informNum()) {
                    QMetaObject::invokeMethod(m_FileSearchTask, "reachInformNum");
                    m_resultNum = 0;
                }
                m_searchController->finishSearchIdCheck();

            } else {
                qDebug() << "Search id changed!";
                m_searchController->finishSearchIdCheck();
                return false;
            }
        }

    } catch(const Xapian::Error &e) {
        qWarning() << QString::fromStdString(e.get_description());
        sendErrorMsg("Xapian Error: " + QString::fromStdString(e.get_description()));
        return false;
    }

    return true;
}

void FileSearchWorker::sendErrorMsg(const QString &msg)
{
    QMetaObject::invokeMethod(m_FileSearchTask, "searchError",
                              Q_ARG(size_t, m_currentSearchId),
                              Q_ARG(QString, msg));
}

bool FileSearchWorker::isOrUnderExtraDir(const QString &path, bool isDirect)
{
    bool res(false);
    QStringList extraDirs = isDirect ? DirWatcher::getDirWatcher()->extraDirsOfPaths(m_validDirectories) : DirWatcher::getDirWatcher()->extraDirs();
    for (const QString& extraDir : extraDirs) {
        if (FileUtils::isOrUnder(path, extraDir)) {
            res = true;
            break;
        }
    }
    return res;
}

bool FileSearchWorker::directSearch()
{
    qDebug() << "Direct search.extradir is" <<  DirWatcher::getDirWatcher()->extraDirsOfPaths(m_validDirectories);
    unsigned int maxResults = m_searchController->maxResults();
    QQueue<QString> searchPathQueue;
    for (QString &dir : m_validDirectories) {
        searchPathQueue.enqueue(dir);
        for (const QString& extraPath : DirWatcher::getDirWatcher()->extraDirsOfPaths(m_validDirectories)) {
            if (extraPath.startsWith(dir + "/") || dir == "/") {
                searchPathQueue.enqueue(extraPath);
            }
        }
    }

    QDir dir;
    QFileInfoList infoList;
    QDir::Filters filters;
    if (m_searchController->isSearchDirOnly()) {
        filters = QDir::Dirs | QDir::NoDotAndDotDot;
    } else {
        filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;
        dir.setSorting(QDir::DirsFirst);
    }
    if(m_searchController->searchHiddenFiles()) {
        filters |= QDir::Hidden;
    }
    dir.setFilter(filters);
    while (!searchPathQueue.empty()) {
        dir.setPath(searchPathQueue.dequeue());
        if (!dir.exists())
            continue;
        infoList = dir.entryInfoList();
        for (const auto &fileInfo : infoList) {
            if (fileInfo.isDir() && !fileInfo.isSymLink()) {
                QString newPath = fileInfo.absoluteFilePath();

                bool inBlackList = std::any_of(m_blackList.begin(), m_blackList.end(), [&] (const QString &dir) {
                    return newPath.startsWith(dir + "/");
                });

                if (underBlocked(newPath)) {
                    continue;
                }

                if (inBlackList && !isOrUnderExtraDir(newPath)) {
                    //在黑名单的路径忽略搜索
                    continue;
                }
                if (m_searchController->isRecurse()) {
                    searchPathQueue.enqueue(newPath);
                }
                if (m_searchController->isSearchFileOnly()) {
                    continue;
                }
            }

            bool matched = false;
            if (m_searchController->getKeyword().isEmpty() && m_searchController->getMatchAllIfNoKeyword()) {
                matched = true;
            } else {
                //同时包含几个key为成功匹配
                for (const QString &keyword: m_searchController->getKeyword()) {
                    if (!keyword.isEmpty()) {
                        //TODO 修改匹配方式,对结果进行排序
                        if (fileInfo.fileName().contains(keyword, Qt::CaseInsensitive)) {
                            matched = true;
                            break;
                        }
                    }
                }
            }

            if (matched && !m_labels.empty()) {
                matched = FileSearchFilter::checkFileLabel(fileInfo.absoluteFilePath(), m_labels);
            }

            if (m_searchController->beginSearchIdCheck(m_currentSearchId)) {
                if (matched) {
                    ResultItem ri(m_currentSearchId);
                    ri.setItemKey(fileInfo.absoluteFilePath());
                    SearchResultProperties properties = m_searchController->getResultProperties(SearchProperty::SearchType::File);
                    if(properties.contains(SearchProperty::SearchResultProperty::FilePath)) {
                        ri.setValue(SearchProperty::SearchResultProperty::FilePath, fileInfo.absoluteFilePath());
                    }
                    if(properties.contains(SearchProperty::SearchResultProperty::FileIconName)) {
                        ri.setValue(SearchProperty::SearchResultProperty::FileIconName, FileUtils::getFileIcon(QUrl::fromLocalFile(fileInfo.absoluteFilePath()).toString()).name());
                    }
                    if(properties.contains(SearchProperty::SearchResultProperty::FileName)) {
                        ri.setValue(SearchProperty::SearchResultProperty::FileName, fileInfo.fileName());
                    }
                    if(properties.contains(SearchProperty::SearchResultProperty::ModifiedTime)) {
                        ri.setValue(SearchProperty::SearchResultProperty::ModifiedTime, fileInfo.lastModified());
                    }
                    m_searchController->getDataQueue()->enqueue(ri);
                    if(++m_resultNum == m_searchController->informNum()) {
                        QMetaObject::invokeMethod(m_FileSearchTask, "reachInformNum");
                        m_resultNum = 0;
                    }
                    --maxResults;
                }
                m_searchController->finishSearchIdCheck();
                if (maxResults == 0) {
                    return true;
                }
            } else {
                qDebug() << "Search id changed!";
                m_searchController->finishSearchIdCheck();
                return false;
            }
        }
    }

    return true;
}

FileSearchFilter::FileSearchFilter(FileSearchWorker *parent) : m_parent(parent) {}

bool FileSearchFilter::operator ()(const Xapian::Document &doc) const
{
    if (m_parent) {
        QString path = QString::fromStdString(doc.get_data());
        if (m_parent->underBlocked(path)) {
            return false;
        }

        bool isRecurse = m_parent->m_searchController->isRecurse();
        bool inSearchDir = true;

        if (!m_parent->m_validDirectories.empty()) {
            inSearchDir = std::any_of(m_parent->m_validDirectories.begin(), m_parent->m_validDirectories.end(), [&](QString &dir) {
                //限制搜索在该目录下
                if (dir != "/" && !path.startsWith(dir + "/")) {
                    return false;
                }

                if (!isRecurse) {
                    //去除搜索路径后，是否包含 "/"
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
                    return !QStringView{path}.mid((dir.length() + 1), (path.length() - dir.length() - 1)).contains(u"/");
#else
                    return !path.midRef((dir.length() + 1), (path.length() - dir.length() - 1)).contains("/");
#endif
                }

                return true;
            });
        }

        bool hasLabel = true;
        if (inSearchDir && !m_parent->m_labels.empty()) {
            hasLabel = FileSearchFilter::checkFileLabel(path, m_parent->m_labels);
        }

        return inSearchDir && hasLabel;
    }

    return true;
}

bool FileSearchFilter::checkFileLabel(const QString &path, const QStringList &labels)
{
    bool hasLabel = false;

    GFile *file = g_file_new_for_path(path.toUtf8().constData());
    if (file) {
        GFileInfo *info = g_file_query_info(file, "metadata::*," G_FILE_ATTRIBUTE_ID_FILE, G_FILE_QUERY_INFO_NONE,
                                            nullptr, nullptr);
        if (info) {
            gboolean hasKey = g_file_info_has_attribute(info, "metadata::peony-file-label-ids");
            if (hasKey) {
                char *fileLabels = g_file_info_get_attribute_as_string(info, "metadata::peony-file-label-ids");
                if (fileLabels) {
                    QStringList fileLabelList = QString::fromUtf8(fileLabels).split("\n");
                    g_free(fileLabels);

                    hasLabel = std::any_of(fileLabelList.begin(), fileLabelList.end(), [&](QString &fileLabel) {
                        return labels.contains(fileLabel);
                    });
                }
            }

            g_object_unref(info);
        }

        g_object_unref(file);
    }

    return hasLabel;
}
