// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include "syntaxhighlighter.h"
#include "syntaxhighlighterdata.h"

#include <QtCore/qregularexpression.h>

#ifdef QQEM_SYNTAX_HIGHLIGHTING_ENABLED
#include <QtQuick3DGlslParser/private/glsllexer_p.h>
#include <QtQuick3DGlslParser/private/glslparser_p.h>
#endif

// Text color and style categories
enum TextStyle : quint8 {
    C_VISUAL_WHITESPACE,
    C_PREPROCESSOR,
    C_NUMBER,
    C_COMMENT,
    C_KEYWORD,
    C_PARAMETER,
    C_REMOVED_LINE,
    C_TAG
};

static constexpr TextStyle GLSLReservedKeyword = C_REMOVED_LINE;

static QTextCharFormat formatForCategory(TextStyle category)
{
    switch (category) {
    case C_PARAMETER:
    {
        QTextCharFormat fmt;
        fmt.setForeground(QColor::fromRgb(0xaa, 0xaa, 0xff));
        return fmt;
    }
    case C_PREPROCESSOR:
    {
        QTextCharFormat fmt;
        fmt.setForeground(QColor::fromRgb(0x55, 0x55, 0xff));
        return fmt;
    }
    case C_NUMBER:
    {
        QTextCharFormat fmt;
        fmt.setForeground(QColor::fromRgb(0xff, 0x55, 0xff));
        return fmt;
    }
    case C_COMMENT:
    {
        QTextCharFormat fmt;
        fmt.setForeground(QColor::fromRgb(0x80, 0x80, 0x80));
        return fmt;
     }
    case C_KEYWORD:
    {
        QTextCharFormat fmt;
        fmt.setForeground(QColor::fromRgb(0xff, 0xff, 0x55));
        return fmt;
    }
    case C_TAG:
    {
        QTextCharFormat fmt;
        fmt.setForeground(QColor::fromRgb(0xaa, 0xff, 0xaa));
        return fmt;
    }
    case C_REMOVED_LINE:
        Q_FALLTHROUGH();
    case C_VISUAL_WHITESPACE:
        break;
    }

    return QTextCharFormat();
}

SyntaxHighlighter::SyntaxHighlighter(QObject *p)
    : QSyntaxHighlighter(p)
{

}

#ifdef QQEM_SYNTAX_HIGHLIGHTING_ENABLED
void SyntaxHighlighter::highlightBlock(const QString &text)
{
    if (m_tagKeywords.isEmpty()) {
        const auto tagKeywords = SyntaxHighlighterData::reservedTagNames();
        for (const auto &kw : tagKeywords)
            m_tagKeywords.insert(kw);
    }
    if (m_argumentKeywords.isEmpty()) {
        const auto args = SyntaxHighlighterData::reservedArgumentNames();
        for (const auto &arg : args)
            m_argumentKeywords.insert(arg);
    }
    const int previousState = previousBlockState();
    int state = 0;
    if (previousState != -1)
        state = previousState & 0xff;

    const QByteArray data = text.toLatin1();
    GLSL::Lexer lex(/*engine=*/ nullptr, data.constData(), data.size());
    lex.setState(state);
    lex.setScanKeywords(false);
    lex.setScanComments(true);

    lex.setVariant(GLSL::Lexer::Variant_GLSL_400);

    QList<GLSL::Token> tokens;
    GLSL::Token tk;
    do {
        lex.yylex(&tk);
        tokens.append(tk);
    } while (tk.isNot(GLSL::Parser::EOF_SYMBOL));

    state = lex.state(); // refresh the state

    if (tokens.isEmpty()) {
        setCurrentBlockState(previousState);
        if (!text.isEmpty()) // the empty line can still contain whitespace
            setFormat(0, text.length(), formatForCategory(C_VISUAL_WHITESPACE));
        return;
    }

    for (int i = 0, end = tokens.size(); i != end; ++i) {
        const GLSL::Token &tk = tokens.at(i);

        int previousTokenEnd = 0;
        if (i != 0) {
            // mark the whitespaces
            previousTokenEnd = tokens.at(i - 1).begin() + tokens.at(i - 1).length;
        }

        if (previousTokenEnd != tk.begin())
            setFormat(previousTokenEnd, tk.begin() - previousTokenEnd, formatForCategory(C_VISUAL_WHITESPACE));

        if (tk.is(GLSL::Parser::T_NUMBER)) {
            setFormat(tk.begin(), tk.length, formatForCategory(C_NUMBER));

        } else if (tk.is(GLSL::Parser::T_IDENTIFIER)) {
            int kind = lex.findKeyword(data.constData() + tk.position, tk.length);
            if (kind == GLSL::Parser::T_IDENTIFIER) {
                if (m_argumentKeywords.contains(QByteArrayView(data.constData() + tk.position, tk.length)))
                    setFormat(tk.position, tk.length, formatForCategory(C_PARAMETER));
            }
            if (kind == GLSL::Parser::T_RESERVED) {
                setFormat(tk.position, tk.length, formatForCategory(GLSLReservedKeyword));
            } else if (kind != GLSL::Parser::T_IDENTIFIER) {
                setFormat(tk.position, tk.length, formatForCategory(C_KEYWORD));
            } else {
                const auto tagStartPos = tk.position - 1;
                bool isTag = data.at(tagStartPos) == '@';
                if (isTag) {
                    const auto tagLength = tk.length + 1;
                    auto line = QByteArrayView(data.constData() + tagStartPos, tagLength).trimmed();
                    auto firstSpace = line.indexOf(' ');
                    QByteArrayView firstWord;
                    if (firstSpace > 0)
                        firstWord = line.sliced(0, firstSpace);
                    if (m_tagKeywords.contains(line) || (!firstWord.isEmpty() && m_tagKeywords.contains(firstWord)))
                        setFormat(tagStartPos, tagLength, formatForCategory(C_TAG));
                }
            }
        } else if (tk.is(GLSL::Parser::T_COMMENT)) {
            highlightLine(text, tk.begin(), tk.length, formatForCategory(C_COMMENT));
        }
    }

    // mark the trailing white spaces
    {
        const GLSL::Token tk = tokens.last();
        const int lastTokenEnd = tk.begin() + tk.length;
        if (text.length() > lastTokenEnd)
            highlightLine(text, lastTokenEnd, text.length() - lastTokenEnd, QTextCharFormat());
    }

    // Multi-line comments
    static QRegularExpression startExpression("/\\*");
    static QRegularExpression endExpression("\\*/");

    setCurrentBlockState(None);

    int startIndex = 0;
    if (previousBlockState() != Comment)
        startIndex = text.indexOf(startExpression);

    // Check multi-line comment blocks
    while (startIndex >= 0) {
        QRegularExpressionMatch endMatch;
        int endIndex = text.indexOf(endExpression, startIndex, &endMatch);
        int commentLength;
        if (endIndex == -1) {
            setCurrentBlockState(Comment);
            commentLength = text.length() - startIndex;
        } else {
            commentLength = endIndex - startIndex
                            + endMatch.capturedLength();
        }
        setFormat(startIndex, commentLength, formatForCategory(C_COMMENT));
        startIndex = text.indexOf(startExpression,
                                  startIndex + commentLength);
    }
}

#else

// Dummy version without glslparser dependency
void SyntaxHighlighter::highlightBlock(const QString &text)
{
    Q_UNUSED(text)
}

#endif

void SyntaxHighlighter::highlightLine(const QString &text, int position, int length,
                                      const QTextCharFormat &format)
{
    const QTextCharFormat visualSpaceFormat = formatForCategory(C_VISUAL_WHITESPACE);

    const int end = position + length;
    int index = position;

    while (index != end) {
        const bool isSpace = text.at(index).isSpace();
        const int start = index;

        do {
            ++index;
        } while (index != end && text.at(index).isSpace() == isSpace);

        const int tokenLength = index - start;
        if (isSpace)
            setFormat(start, tokenLength, visualSpaceFormat);
        else if (format.isValid())
            setFormat(start, tokenLength, format);
    }
}

QQuickTextDocument *SyntaxHighlighter::document() const
{
    return m_quickTextDocument;
}

void SyntaxHighlighter::setDocument(QQuickTextDocument *newDocument)
{
    if (m_quickTextDocument == newDocument)
        return;

    m_quickTextDocument = newDocument;
    QSyntaxHighlighter::setDocument(m_quickTextDocument != nullptr ? m_quickTextDocument->textDocument() : nullptr);

    emit documentChanged();
}
