#ifndef VSEARCHCONFIG_H
#define VSEARCHCONFIG_H

#include <QString>
#include <QStringList>
#include <QSharedPointer>
#include <QVector>
#include <QRegExp>

#include "utils/vutils.h"


struct VSearchToken
{
    enum Type
    {
        RawString = 0,
        RegularExpression
    };

    enum Operator
    {
        And = 0,
        Or
    };

    VSearchToken()
        : m_type(Type::RawString),
          m_op(Operator::And),
          m_caseSensitivity(Qt::CaseSensitive)
    {
    }

    void clear()
    {
        m_keywords.clear();
        m_regs.clear();
    }

    void append(const QString &p_rawStr)
    {
        m_keywords.append(p_rawStr);
    }

    void append(const QRegExp &p_reg)
    {
        m_regs.append(p_reg);
    }

    QString toString() const
    {
        return QString("token %1 %2 %3 %4 %5").arg(m_type)
                                              .arg(m_op)
                                              .arg(m_caseSensitivity)
                                              .arg(m_keywords.size())
                                              .arg(m_regs.size());
    }

    // Whether @p_text match all the constraint.
    bool matched(const QString &p_text) const
    {
        int size = m_keywords.size();
        if (m_type == Type::RegularExpression) {
            size = m_regs.size();
        }

        if (size == 0) {
            return false;
        }

        bool ret = m_op == Operator::And ? true : false;
        for (int i = 0; i < size; ++i) {
            bool tmp = false;
            if (m_type == Type::RawString) {
                tmp = p_text.contains(m_keywords[i], m_caseSensitivity);
            } else {
                tmp = p_text.contains(m_regs[i]);
            }

            if (tmp) {
                if (m_op == Operator::Or) {
                    ret = true;
                    break;
                }
            } else {
                if (m_op == Operator::And) {
                    ret = false;
                    break;
                }
            }
        }

        return ret;
    }

    void startBatchMode()
    {
        int size = m_type == Type::RawString ? m_keywords.size() : m_regs.size();
        m_matchesInBatch.resize(size);
        m_matchesInBatch.fill(false);
        m_numOfMatches = 0;
    }

    // Match one string in batch mode.
    // Returns true if @p_text matches one.
    bool matchBatchMode(const QString &p_text)
    {
        bool ret = false;
        int size = m_matchesInBatch.size();
        for (int i = 0; i < size; ++i) {
            if (m_matchesInBatch[i]) {
                continue;
            }

            bool tmp = false;
            if (m_type == Type::RawString) {
                tmp = p_text.contains(m_keywords[i], m_caseSensitivity);
            } else {
                tmp = p_text.contains(m_regs[i]);
            }

            if (tmp) {
                m_matchesInBatch[i] = true;
                ++m_numOfMatches;
                ret = true;
            }
        }

        return ret;
    }

    // Whether it is OK to finished batch mode.
    // @p_matched: the overall match result.
    bool readyToEndBatchMode(bool &p_matched) const
    {
        if (m_op == VSearchToken::And) {
            // We need all the tokens matched.
            if (m_numOfMatches == m_matchesInBatch.size()) {
                p_matched = true;
                return true;
            } else {
                p_matched = false;
                return false;
            }
        } else {
            // We only need one match.
            if (m_numOfMatches > 0) {
                p_matched = true;
                return true;
            } else {
                p_matched = false;
                return false;
            }
        }
    }

    void endBatchMode()
    {
        m_matchesInBatch.clear();
        m_numOfMatches = 0;
    }

    int tokenSize() const
    {
        return m_type == Type::RawString ? m_keywords.size() : m_regs.size();
    }

    bool isEmpty() const
    {
        return tokenSize() == 0;
    }

    bool operator==(const VSearchToken &p_other) const
    {
        if (m_type != p_other.m_type
            || m_op != p_other.m_op
            || m_caseSensitivity != p_other.m_caseSensitivity
            || m_keywords.size() != p_other.m_keywords.size()
            || m_numOfMatches != p_other.m_numOfMatches
            || m_regs.size() != p_other.m_regs.size()) {
            return false;
        }

        return m_keywords == p_other.m_keywords
               && m_regs == p_other.m_regs;
    }

    VSearchToken::Type m_type;

    VSearchToken::Operator m_op;

    Qt::CaseSensitivity m_caseSensitivity;

    // Valid at RawString.
    QVector<QString> m_keywords;

    // Valid at RegularExpression.
    QVector<QRegExp> m_regs;

    // Bitmap for batch mode.
    // True if m_regs[i] or m_keywords[i] has been matched.
    QVector<bool> m_matchesInBatch;

    int m_numOfMatches;
};


struct VSearchConfig
{
    enum Scope
    {
        NoneScope = 0,
        CurrentNote,
        OpenedNotes,
        CurrentFolder,
        CurrentNotebook,
        AllNotebooks,
        ExplorerDirectory
    };

    enum Object
    {
        NoneObject = 0,
        Name = 0x1UL,
        Content = 0x2UL,
        Outline = 0x4UL,
        Tag = 0x8UL,
        Path = 0x10UL
    };

    enum Target
    {
        NoneTarget = 0,
        Note = 0x1UL,
        Folder = 0x2UL,
        Notebook = 0x4UL
    };

    enum Engine
    {
        Internal = 0
    };

    enum Option
    {
        NoneOption = 0,
        CaseSensitive = 0x1UL,
        WholeWordOnly = 0x2UL,
        Fuzzy = 0x4UL,
        RegularExpression = 0x8UL
    };


    VSearchConfig()
        : VSearchConfig(Scope::NoneScope,
                        Object::NoneObject,
                        Target::NoneTarget,
                        Engine::Internal,
                        Option::NoneOption,
                        "",
                        "")
    {
    }


    VSearchConfig(int p_scope,
                  int p_object,
                  int p_target,
                  int p_engine,
                  int p_option,
                  const QString &p_keyword,
                  const QString &p_pattern)
        : m_scope(p_scope),
          m_object(p_object),
          m_target(p_target),
          m_engine(p_engine),
          m_option(p_option),
          m_pattern(p_pattern)
    {
        compileToken(p_keyword);
    }

    // We support some magic switch in the keyword which will suppress the specified
    // options:
    // \c: Case insensitive;
    // \C: Case sensitive;
    // \r: Turn off regular expression;
    // \R: Turn on regular expression;
    // \f: Turn off fuzzy search;
    // \F: Turn on fuzzy search (invalid when searching content);
    // \w: Turn off whole word only;
    // \W: Turn on whole word only;
    void compileToken(const QString &p_keyword)
    {
        m_token.clear();
        m_contentToken.clear();
        if (p_keyword.isEmpty()) {
            return;
        }

        // """ to input a ";
        // && for AND, || for OR;
        QStringList args = VUtils::parseCombinedArgString(p_keyword);

        Qt::CaseSensitivity cs = m_option & VSearchConfig::CaseSensitive
                                 ? Qt::CaseSensitive : Qt::CaseInsensitive;
        bool useReg = m_option & VSearchConfig::RegularExpression;
        bool wwo = m_option & VSearchConfig::WholeWordOnly;
        bool fuzzy = m_option & VSearchConfig::Fuzzy;

        // Read magic switch from keyword.
        for (int i = 0; i < args.size();) {
            const QString &arg = args[i];
            if (arg.size() != 2 || arg[0] != '\\') {
                ++i;
                continue;
            }

            if (arg == "\\c") {
                cs = Qt::CaseInsensitive;
            } else if (arg == "\\C") {
                cs = Qt::CaseSensitive;
            } else if (arg == "\\r") {
                useReg = false;
            } else if (arg == "\\R") {
                useReg = true;
            } else if (arg == "\\f") {
                fuzzy = false;
            } else if (arg == "\\F") {
                fuzzy = true;
            } else if (arg == "\\w") {
                wwo = false;
            } else if (arg == "\\W") {
                wwo = true;
            } else {
                ++i;
                continue;
            }

            args.removeAt(i);
        }

        if (args.isEmpty()) {
            return;
        }

        m_token.m_caseSensitivity = cs;
        m_contentToken.m_caseSensitivity = cs;

        if (useReg) {
            m_token.m_type = VSearchToken::RegularExpression;
            m_contentToken.m_type = VSearchToken::RegularExpression;
        } else {
            if (fuzzy) {
                m_token.m_type = VSearchToken::RegularExpression;
                m_contentToken.m_type = VSearchToken::RawString;
            } else if (wwo) {
                m_token.m_type = VSearchToken::RegularExpression;
                m_contentToken.m_type = VSearchToken::RegularExpression;
            } else {
                m_token.m_type = VSearchToken::RawString;
                m_contentToken.m_type = VSearchToken::RawString;
            }
        }

        VSearchToken::Operator op = VSearchToken::And;
        for (auto const & arg : args) {
            if (arg == QStringLiteral("&&")) {
                op = VSearchToken::And;
                continue;
            } else if (arg == QStringLiteral("||")) {
                op = VSearchToken::Or;
                continue;
            }

            if (useReg) {
                QRegExp reg(arg, cs);
                m_token.append(reg);
                m_contentToken.append(reg);
            } else {
                if (fuzzy) {
                    QString wildcardText(arg.size() * 2 + 1, '*');
                    for (int i = 0, j = 1; i < arg.size(); ++i, j += 2) {
                        wildcardText[j] = arg[i];
                    }

                    QRegExp reg(wildcardText, cs, QRegExp::Wildcard);
                    m_token.append(reg);
                    m_contentToken.append(arg);
                } else if (wwo) {
                    QString pattern = QRegExp::escape(arg);
                    pattern = "\\b" + pattern + "\\b";

                    QRegExp reg(pattern, cs);
                    m_token.append(reg);
                    m_contentToken.append(reg);
                } else {
                    m_token.append(arg);
                    m_contentToken.append(arg);
                }
            }
        }

        m_token.m_op = op;
        m_contentToken.m_op = op;
    }

    bool isEmpty() const
    {
        return m_token.tokenSize() == 0;
    }

    QStringList toConfig() const
    {
        QStringList str;
        str << QString::number(m_scope);
        str << QString::number(m_object);
        str << QString::number(m_target);
        str << QString::number(m_engine);
        str << QString::number(m_option);
        str << m_pattern;

        return str;
    }

    static VSearchConfig fromConfig(const QStringList &p_str)
    {
        VSearchConfig config;
        if (p_str.size() != 6) {
            return config;
        }

        config.m_scope = p_str[0].toInt();
        config.m_object = p_str[1].toInt();
        config.m_target = p_str[2].toInt();
        config.m_engine = p_str[3].toInt();
        config.m_option = p_str[4].toInt();
        config.m_pattern = p_str[5];

        return config;
    }

    int m_scope;
    int m_object;
    int m_target;
    int m_engine;
    int m_option;

    // Wildcard pattern to filter file.
    QString m_pattern;

    // Token for name, outline.
    VSearchToken m_token;

    // Token for content and tag.
    VSearchToken m_contentToken;
};


struct VSearchResultSubItem
{
    VSearchResultSubItem()
        : m_lineNumber(-1)
    {
    }

    VSearchResultSubItem(int p_lineNumber,
                         const QString &p_text)
        : m_lineNumber(p_lineNumber),
          m_text(p_text)
    {
    }

    int m_lineNumber;

    QString m_text;
};


struct VSearchResultItem
{
    enum ItemType
    {
        None = 0,
        Note,
        Folder,
        Notebook
    };


    enum MatchType
    {
        LineNumber = 0,
        OutlineIndex
    };


    VSearchResultItem()
        : m_type(ItemType::None),
          m_matchType(MatchType::LineNumber)
    {
    }

    VSearchResultItem(VSearchResultItem::ItemType p_type,
                      VSearchResultItem::MatchType p_matchType,
                      const QString &p_text,
                      const QString &p_path,
                      const QSharedPointer<VSearchConfig> &p_config = nullptr)
        : m_type(p_type),
          m_matchType(p_matchType),
          m_text(p_text),
          m_path(p_path),
          m_config(p_config)
    {
    }

    bool isEmpty() const
    {
        return m_type == ItemType::None;
    }

    QString toString() const
    {
        return QString("item text: [%1] path: [%2] subitems: %3")
                      .arg(m_text)
                      .arg(m_path)
                      .arg(m_matches.size());
    }


    ItemType m_type;

    MatchType m_matchType;

    // Text to displayed. If empty, use @m_path instead.
    QString m_text;

    // Path of the target.
    QString m_path;

    // Matched places within this item.
    QList<VSearchResultSubItem> m_matches;

    // Search config to search for this item.
    QSharedPointer<VSearchConfig> m_config;
};


class VSearch;


enum class VSearchState
{
    Idle = 0,
    Busy,
    Success,
    Fail,
    Cancelled
};


struct VSearchResult
{
    friend class VSearch;

    explicit VSearchResult(VSearch *p_search)
        : m_state(VSearchState::Idle),
          m_search(p_search)
    {
    }

    bool hasError() const
    {
        return !m_errMsg.isEmpty();
    }

    void logError(const QString &p_err)
    {
        if (m_errMsg.isEmpty()) {
            m_errMsg = p_err;
        } else {
            m_errMsg = "\n" + p_err;
        }
    }

    void addSecondPhaseItem(const QString &p_item)
    {
        m_secondPhaseItems.append(p_item);
    }

    QString toString() const
    {
        QString str = QString("search result: state %1 err %2")
                             .arg((int)m_state)
                             .arg(!m_errMsg.isEmpty());
        return str;
    }

    bool hasSecondPhaseItems() const
    {
        return !m_secondPhaseItems.isEmpty();
    }

    VSearchState m_state;

    QString m_errMsg;

    QStringList m_secondPhaseItems;

private:
    VSearch *m_search;
};

#endif // VSEARCHCONFIG_H
