#include "include/Search/filesearcher.h"

#include "include/Search/searchhelpers.h"
#include "include/Search/searchstring.h"
#include "include/docengine.h"

#include <QDirIterator>

#include <algorithm>

/**
 * @brief matchesWholeWord Returns true if the substring at data.mid(index,matchLength) is a whole word.
 *                         This means it's preceded and followed by either a whitespace, symbol or punctuation.
 */
bool matchesWholeWord(int index, int matchLength, const QString& data)
{
    QChar boundary;

    if (index != 0) {
        boundary = data[index - 1];
        if (!boundary.isPunct() && !boundary.isSpace() && !boundary.isSymbol()) {
            return false;
        }
    }
    if (data.length() != index + matchLength) {
        boundary = data[index + matchLength];
        if (!boundary.isPunct() && !boundary.isSpace() && !boundary.isSymbol()) {
            return false;
        }
    }
    return true;
}

/**
 * @brief trimEnd Returns the string with all whitespace trimmed from the end.
 *                Taken from the QString source code.
 */
QString trimEnd(const QString& str)
{
    const QChar* begin = str.cbegin();
    const QChar* end = str.cend();

    while (begin < end && end[-1].isSpace())
        end--;

    return QString(begin, end - begin);
}

const int MatchResult::CUTOFF_LENGTH = 60;

FileSearcher::FileSearcher(const SearchConfig& config)
    : QThread(nullptr)
    , m_searchConfig(config)
{
}

FileSearcher* FileSearcher::prepareAsyncSearch(const SearchConfig& config)
{ return new FileSearcher(config); }

QRegularExpression FileSearcher::createRegexFromConfig(const SearchConfig& config)
{
    QRegularExpression regex;

    const QFlags<QRegularExpression::PatternOption> options =
        config.matchCase ? QRegularExpression::MultilineOption
                         : QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption;

    const QString regexString = config.matchWord ? "\\b" + config.searchString + "\\b" : config.searchString;

    regex.setPattern(regexString);
    regex.setPatternOptions(options);

    return regex;
}

DocResult FileSearcher::searchPlainText(const SearchConfig& config, const QString& content)
{
    DocResult results;

    const Qt::CaseSensitivity caseSense = config.matchCase ? Qt::CaseSensitive : Qt::CaseInsensitive;
    const std::vector<int> linePosition = SearchHelpers::linePositions(content);
    const QString searchString = (config.searchMode == SearchConfig::ModePlainTextSpecialChars)
                                     ? SearchString::unescape(config.searchString)
                                     : config.searchString;

    const int matchLength = searchString.length();
    int offset = 0;

    while ((offset = content.indexOf(searchString, offset, caseSense)) != -1) {
        if (config.matchWord && !matchesWholeWord(offset, matchLength, content)) {
            offset += matchLength;
            continue;
        }

        // std::upper_bound returns an iterator to the first item greater than 'offset'. This is the line after the
        // match. Subtract 1 to get the line the match is on.
        const auto it = std::upper_bound(linePosition.begin(), linePosition.end(), offset);
        const int line = std::distance(linePosition.begin(), it);
        const int lineStart = linePosition[line - 1];
        const int lineEnd = linePosition[line];

        MatchResult result;
        result.lineNumber = line;
        result.matchLineString = trimEnd(content.mid(lineStart, lineEnd - lineStart));
        result.positionInFile = offset;
        result.positionInLine = offset - lineStart;
        result.matchLength = matchLength;
        results.results.push_back(result);

        offset += matchLength;
    }

    return results;
}

DocResult FileSearcher::searchRegExp(const QRegularExpression& regex, const QString& content)
{
    DocResult results;

    int offset = 0;
    std::vector<int> linePosition = SearchHelpers::linePositions(content);

    QRegularExpressionMatch match;
    for (;;) {
        match = regex.match(content, offset);

        if (!match.hasMatch())
            break;

        offset = match.capturedStart();
        const auto it = std::upper_bound(linePosition.begin(), linePosition.end(), offset);
        const int line = std::distance(linePosition.begin(), it);
        const int lineStart = linePosition[line - 1];
        const int lineEnd = linePosition[line];

        MatchResult result;
        result.lineNumber = line;
        result.matchLineString = trimEnd(content.mid(lineStart, lineEnd - lineStart));
        result.positionInFile = offset;
        result.positionInLine = offset - lineStart;
        result.matchLength = match.capturedLength();
        result.regexMatch = match;
        results.results.push_back(result);

        // Advance at least by one to avoit infinite loops when capturing
        // empty expressions.
        offset += std::max(1, result.matchLength);
    }

    results.regexCaptureGroupCount = regex.captureCount();

    return results;
}

void FileSearcher::run()
{
    if (m_searchConfig.searchMode == SearchConfig::ModeRegex) {
        m_regex = createRegexFromConfig(m_searchConfig);
    } else if (m_searchConfig.searchMode == SearchConfig::ModePlainTextSpecialChars) {
        m_searchConfig.searchString = SearchString::unescape(m_searchConfig.searchString);
    }

    const QFlags<QDirIterator::IteratorFlag> dirIteratorOptions =
        m_searchConfig.includeSubdirs ? (QDirIterator::Subdirectories | QDirIterator::FollowSymlinks)
                                      : QDirIterator::NoIteratorFlags;

    // Split contents of the file pattern string and sanitize it for use
    QStringList filters = m_searchConfig.filePattern.split(',', Qt::SkipEmptyParts);
    for (QString& item : filters)
        item = item.trimmed();

    // Create a list of all files that will be read.
    QDirIterator it(m_searchConfig.directory, filters, QDir::Files | QDir::Readable | QDir::Hidden, dirIteratorOptions);
    QStringList fileList;

    while (it.hasNext())
        fileList << it.next();

    const int listSize = fileList.size();
    emit resultProgress(0, listSize);

    // Start the actual search
    int count = 0;
    for (const auto& fileName : fileList) {
        if (isInterruptionRequested())
            break;

        if (++count % 100 == 0)
            emit resultProgress(count, listSize);

        QFile f(fileName);
        DocEngine::DecodedText decodedText;
        decodedText = DocEngine::readToString(&f);
        f.close();

        if (decodedText.error) {
            // File could not be read. We'll ignore this error since it should never happen. QDirIterator only iterates
            // over readable files and DocEngine only reads the file. But if it happens we can skip the rest, just in
            // case.
            continue;
        }

        DocResult res;
        switch (m_searchConfig.searchMode) {
        case SearchConfig::ModePlainText:
        case SearchConfig::ModePlainTextSpecialChars:
            res = searchPlainText(m_searchConfig, decodedText.text);
            break;
        case SearchConfig::ModeRegex:
            res = searchRegExp(m_regex, decodedText.text);
            break;
        }

        if (!res.results.empty()) {
            res.docType = DocResult::TypeFile;
            res.fileName = fileName;
            m_searchResult.results.push_back(res);
        }
    }

    emit resultReady();
}
