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

#include "syntaxhighlighter.h"

#include <QtCore/qregularexpression.h>

#include <QtQuick3DRuntimeRender/private/qssgshadermaterialadapter_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrenderdefaultmaterialshadergenerator_p.h>

#include <QtQuick3DGlslParser/private/glsllexer_p.h>
#include <QtQuick3DGlslParser/private/glslparser_p.h>

QT_BEGIN_NAMESPACE

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

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(0x55, 0xff, 0xff));
        return fmt;
     }
    case C_KEYWORD:
    {
        QTextCharFormat fmt;
        fmt.setForeground(QColor::fromRgb(0xff, 0xff, 0x55));
        return fmt;
    }
    case C_REMOVED_LINE:
        Q_FALLTHROUGH();
    case C_VISUAL_WHITESPACE:
        break;
    }

    return QTextCharFormat();
}

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

}

void SyntaxHighlighter::highlightBlock(const QString &text)
{
    if (m_keywords.isEmpty()) {
        const auto keywords = QtQuick3DEditorHelpers::CustomMaterial::preprocessorVars();
        for (const auto &kw : keywords)
            m_keywords.insert(kw);
    }
    if (m_argumentKeywords.isEmpty()) {
        const auto args = QtQuick3DEditorHelpers::CustomMaterial::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_COMMENT)) {
            highlightLine(text, tk.begin(), tk.length, formatForCategory(C_COMMENT));
        } 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_keywords.contains(QByteArrayView(data.constData() + tk.position, tk.length)))
                    setFormat(tk.position, tk.length, formatForCategory(C_PREPROCESSOR));
                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));
        }
    }

    // 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());
    }
}

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();
}

QT_END_NAMESPACE
