/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtSCriptTools module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qscriptsyntaxhighlighter_p.h"

#include <algorithm>

#ifndef QT_NO_SYNTAXHIGHLIGHTER

QT_BEGIN_NAMESPACE

enum ScriptIds {
    Comment = 1,
    Number,
    String,
    Type,
    Keyword,
    PreProcessor,
    Label
};

#define MAX_KEYWORD 63
static const char *const keywords[MAX_KEYWORD] = {
    "Infinity",
    "NaN",
    "abstract",
    "boolean",
    "break",
    "byte",
    "case",
    "catch",
    "char",
    "class",
    "const",
    "constructor",
    "continue",
    "debugger",
    "default",
    "delete",
    "do",
    "double",
    "else",
    "enum",
    "export",
    "extends",
    "false",
    "final",
    "finally",
    "float",
    "for",
    "function",
    "goto",
    "if",
    "implements",
    "import",
    "in",
    "instanceof",
    "int",
    "interface",
    "long",
    "native",
    "new",
    "package",
    "private",
    "protected",
    "public",
    "return",
    "short",
    "static",
    "super",
    "switch",
    "synchronized",
    "this",
    "throw",
    "throws",
    "transient",
    "true",
    "try",
    "typeof",
    "undefined",
    "var",
    "void",
    "volatile",
    "while",
    "with",    // end of array
    0
};

struct KeywordHelper
{
    inline KeywordHelper(const QString &word) : needle(word) {}
    const QString needle;
};

static bool operator<(const KeywordHelper &helper, const char *kw)
{
    return helper.needle < QLatin1String(kw);
}

static bool operator<(const char *kw, const KeywordHelper &helper)
{
    return QLatin1String(kw) < helper.needle;
}

static bool isKeyword(const QString &word)
{
    const char * const *start = &keywords[0];
    const char * const *end = &keywords[MAX_KEYWORD - 1];

    const KeywordHelper keywordHelper(word);
    const char * const *kw = std::lower_bound(start, end, keywordHelper);

    return kw != end && !(keywordHelper < *kw);
}

QScriptSyntaxHighlighter::QScriptSyntaxHighlighter(QTextDocument *document)
    : QSyntaxHighlighter(document)
{

    m_formats[ScriptNumberFormat].setForeground(Qt::darkBlue);
    m_formats[ScriptStringFormat].setForeground(Qt::darkGreen);
    m_formats[ScriptTypeFormat].setForeground(Qt::darkMagenta);
    m_formats[ScriptKeywordFormat].setForeground(Qt::darkYellow);
    m_formats[ScriptPreprocessorFormat].setForeground(Qt::darkBlue);
    m_formats[ScriptLabelFormat].setForeground(Qt::darkRed);
    m_formats[ScriptCommentFormat].setForeground(Qt::darkGreen);
    m_formats[ScriptCommentFormat].setFontItalic(true);
}

QScriptSyntaxHighlighter::~QScriptSyntaxHighlighter()
{
}

void QScriptSyntaxHighlighter::highlightBlock(const QString &text)
{

    // states
    enum States { StateStandard, StateCommentStart1, StateCCommentStart2,
                  StateScriptCommentStart2, StateCComment, StateScriptComment, StateCCommentEnd1,
                  StateCCommentEnd2, StateStringStart, StateString, StateStringEnd,
                  StateString2Start, StateString2, StateString2End,
                  StateNumber, StatePreProcessor, NumStates  };

    // tokens
    enum Tokens { InputAlpha, InputNumber, InputAsterix, InputSlash, InputParen,
                  InputSpace, InputHash, InputQuotation, InputApostrophe, InputSep, NumTokens };

    static uchar table[NumStates][NumTokens] = {
        { StateStandard,      StateNumber,     StateStandard,       StateCommentStart1,    StateStandard,   StateStandard,   StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateStandard
        { StateStandard,      StateNumber,   StateCCommentStart2, StateScriptCommentStart2, StateStandard,   StateStandard,   StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateCommentStart1
        { StateCComment,      StateCComment,   StateCCommentEnd1,   StateCComment,         StateCComment,   StateCComment,   StateCComment,     StateCComment,    StateCComment,     StateCComment }, // StateCCommentStart2
        { StateScriptComment,    StateScriptComment, StateScriptComment,     StateScriptComment,       StateScriptComment, StateScriptComment, StateScriptComment,   StateScriptComment,  StateScriptComment,   StateScriptComment }, // ScriptCommentStart2
        { StateCComment,      StateCComment,   StateCCommentEnd1,   StateCComment,         StateCComment,   StateCComment,   StateCComment,     StateCComment,    StateCComment,     StateCComment }, // StateCComment
        { StateScriptComment,    StateScriptComment, StateScriptComment,     StateScriptComment,       StateScriptComment, StateScriptComment, StateScriptComment,   StateScriptComment,  StateScriptComment,   StateScriptComment }, // StateScriptComment
        { StateCComment,      StateCComment,   StateCCommentEnd1,   StateCCommentEnd2,     StateCComment,   StateCComment,   StateCComment,     StateCComment,    StateCComment,     StateCComment }, // StateCCommentEnd1
        { StateStandard,      StateNumber,     StateStandard,       StateCommentStart1,    StateStandard,   StateStandard,   StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateCCommentEnd2
        { StateString,        StateString,     StateString,         StateString,           StateString,     StateString,     StateString,       StateStringEnd,   StateString,       StateString }, // StateStringStart
        { StateString,        StateString,     StateString,         StateString,           StateString,     StateString,     StateString,       StateStringEnd,   StateString,       StateString }, // StateString
        { StateStandard,      StateStandard,   StateStandard,       StateCommentStart1,    StateStandard,   StateStandard,   StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateStringEnd
        { StateString2,       StateString2,    StateString2,        StateString2,          StateString2,    StateString2,    StateString2,      StateString2,     StateString2End,   StateString2 }, // StateString2Start
        { StateString2,       StateString2,    StateString2,        StateString2,          StateString2,    StateString2,    StateString2,      StateString2,     StateString2End,   StateString2 }, // StateString2
        { StateStandard,      StateStandard,   StateStandard,       StateCommentStart1,    StateStandard,   StateStandard,   StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateString2End
        { StateNumber,        StateNumber,     StateStandard,       StateCommentStart1,    StateStandard,   StateStandard,   StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateNumber
        { StatePreProcessor,  StateStandard,   StateStandard,       StateCommentStart1,    StateStandard,   StateStandard,   StatePreProcessor, StateStringStart, StateString2Start, StateStandard } // StatePreProcessor
    };

    QString buffer;
    buffer.reserve(text.length());

    QTextCharFormat emptyFormat;

    int state = StateStandard;
    int braceDepth = 0;
    const int previousState = previousBlockState();
    if (previousState != -1) {
        state = previousState & 0xff;
        braceDepth = previousState >> 8;
    }

    if (text.isEmpty()) {
        setCurrentBlockState(previousState);
#if 0
        TextEditDocumentLayout::clearParentheses(currentBlock());
#endif
        return;
    }
#if 0
    Parentheses parentheses;
    parentheses.reserve(20); // assume wizard level ;-)
#endif
    int input = -1;
    int i = 0;
    bool lastWasBackSlash = false;
    bool makeLastStandard = false;

    static const QString alphabeth = QLatin1String("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
    static const QString mathChars = QLatin1String("xXeE");
    static const QString numbers = QLatin1String("0123456789");
    bool questionMark = false;
    QChar lastChar;

    int firstNonSpace = -1;

    for (;;) {
        const QChar c = text.at(i);

        if (lastWasBackSlash) {
            input = InputSep;
        } else {
            switch (c.toLatin1()) {
                case '*':
                    input = InputAsterix;
                    break;
                case '/':
                    input = InputSlash;
                    break;
                case '{':
                    braceDepth++;
                    // fall through
                case '(': case '[':
                    input = InputParen;
                    switch (state) {
                    case StateStandard:
                    case StateNumber:
                    case StatePreProcessor:
                    case StateCCommentEnd2:
                    case StateCCommentEnd1:
                    case StateString2End:
                    case StateStringEnd:
//                        parentheses.push_back(Parenthesis(Parenthesis::Opened, c, i));
                        break;
                    default:
                        break;
                    }
                    break;
                case '}':
                    if (--braceDepth < 0)
                        braceDepth = 0;
                    // fall through
                case ')': case ']':
                    input = InputParen;
                    switch (state) {
                    case StateStandard:
                    case StateNumber:
                    case StatePreProcessor:
                    case StateCCommentEnd2:
                    case StateCCommentEnd1:
                    case StateString2End:
                    case StateStringEnd:
//                        parentheses.push_back(Parenthesis(Parenthesis::Closed, c, i));
                        break;
                    default:
                        break;
                    }
                    break;
                case '#':
                    input = InputHash;
                    break;
                case '"':
                    input = InputQuotation;
                    break;
                case '\'':
                    input = InputApostrophe;
                    break;
                case ' ':
                    input = InputSpace;
                    break;
                case '1': case '2': case '3': case '4': case '5':
                case '6': case '7': case '8': case '9': case '0':
                    if (alphabeth.contains(lastChar)
                        && (!mathChars.contains(lastChar) || !numbers.contains(text.at(i - 1)))
                       ) {
                        input = InputAlpha;
                    } else {
                        if (input == InputAlpha && numbers.contains(lastChar))
                            input = InputAlpha;
                        else
                            input = InputNumber;
                    }
                    break;
                case ':': {
                              input = InputAlpha;
                              const QChar colon = QLatin1Char(':');
                              if (state == StateStandard && !questionMark && lastChar != colon) {
                                  const QChar nextChar = i < text.length() - 1 ?  text.at(i + 1) : QLatin1Char(' ');
                                  if (nextChar != colon)
                                      for (int j = 0; j < i; ++j) {
                                          if (format(j) == emptyFormat )
                                              setFormat(j, 1, m_formats[ScriptLabelFormat]);
                                      }
                              }
                          } break;
                default:
                    if (!questionMark && c == QLatin1Char('?'))
                        questionMark = true;
                    if (c.isLetter() || c == QLatin1Char('_'))
                        input = InputAlpha;
                    else
                      input = InputSep;
                break;
            }
        }

        if (input != InputSpace) {
            if (firstNonSpace < 0)
                firstNonSpace = i;
         }

        lastWasBackSlash = !lastWasBackSlash && c == QLatin1Char('\\');

        if (input == InputAlpha)
            buffer += c;

        state = table[state][input];

        switch (state) {
            case StateStandard: {
                                    setFormat(i, 1, emptyFormat);
                                    if (makeLastStandard)
                                        setFormat(i - 1, 1, emptyFormat);
                                    makeLastStandard = false;
                                    if (input != InputAlpha) {
                                        highlightWord(i, buffer);
                                        buffer = QString();
                                    }
                                } break;
            case StateCommentStart1:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = true;
                                buffer = QString();
                                break;
            case StateCCommentStart2:
                                setFormat(i - 1, 2, m_formats[ScriptCommentFormat]);
                                makeLastStandard = false;
//                                parentheses.push_back(Parenthesis(Parenthesis::Opened, QLatin1Char('/'), i-1));
                                buffer = QString();
                                break;
            case StateScriptCommentStart2:
                                setFormat(i - 1, 2, m_formats[ScriptCommentFormat]);
                                makeLastStandard = false;
                                buffer = QString();
                                break;
            case StateCComment:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, m_formats[ScriptCommentFormat]);
                                buffer = QString();
                                break;
            case StateScriptComment:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, m_formats[ScriptCommentFormat]);
                                buffer = QString();
                                break;
            case StateCCommentEnd1:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, m_formats[ScriptCommentFormat]);
                                buffer = QString();
                                break;
            case StateCCommentEnd2:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, m_formats[ScriptCommentFormat]);
//                                parentheses.push_back(Parenthesis(Parenthesis::Closed, QLatin1Char('/'), i));
                                buffer = QString();
                                break;
            case StateStringStart:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, emptyFormat);
                                buffer = QString();
                                break;
            case StateString:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, m_formats[ScriptStringFormat]);
                                buffer = QString();
                                break;
            case StateStringEnd:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, emptyFormat);
                                buffer = QString();
                                break;
            case StateString2Start:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, emptyFormat);
                                buffer = QString();
                                break;
            case StateString2:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, m_formats[ScriptStringFormat]);
                                buffer = QString();
                                break;
            case StateString2End:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, emptyFormat);
                                buffer = QString();
                                break;
            case StateNumber:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, m_formats[ScriptNumberFormat]);
                                buffer = QString();
                                break;
            case StatePreProcessor:
                                if (makeLastStandard)
                                    setFormat(i - 1, 1, emptyFormat);
                                makeLastStandard = false;
                                setFormat(i, 1, m_formats[ScriptPreprocessorFormat]);
                                buffer = QString();
                                break;
        }

        lastChar = c;
        i++;
        if (i >= text.length()) {
#if 0
            if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(currentBlock())) {
                userData->setHasClosingCollapse(false);
                userData->setCollapseMode(TextBlockUserData::NoCollapse);
            }
            int collapse = Parenthesis::collapseAtPos(parentheses);
            if (collapse >= 0) {
                if (collapse == firstNonSpace)
                    TextEditDocumentLayout::userData(currentBlock())->setCollapseMode(TextBlockUserData::CollapseThis);
                else
                    TextEditDocumentLayout::userData(currentBlock())->setCollapseMode(TextBlockUserData::CollapseAfter);
            }
            if (Parenthesis::hasClosingCollapse(parentheses)) {
                TextEditDocumentLayout::userData(currentBlock())->setHasClosingCollapse(true);
            }
#endif

            break;
        }
    }

    highlightWord(text.length(), buffer);

    switch (state) {
    case  StateCComment:
    case  StateCCommentEnd1:
    case  StateCCommentStart2:
        state = StateCComment;
        break;
    case StateString:
        // quotes cannot span multiple lines, so if somebody starts
        // typing a quoted string we don't need to look for the ending
        // quote in another line (or highlight until the end of the
        // document) and therefore slow down editing.
        state = StateStandard;
        break;
    case StateString2:
        state =  StateStandard;
        break;
    default:
        state = StateStandard;
        break;
    }

#if 0
    TextEditDocumentLayout::setParentheses(currentBlock(), parentheses);
#endif

    setCurrentBlockState((braceDepth << 8) | state);
}

void QScriptSyntaxHighlighter::highlightWord(int currentPos, const QString &buffer)
{
    if (buffer.isEmpty())
        return;

    // try to highlight Qt 'identifiers' like QObject and Q_PROPERTY
    // but don't highlight words like 'Query'
    if (buffer.length() > 1
        && buffer.at(0) == QLatin1Char('Q')
        && (buffer.at(1).isUpper()
            || buffer.at(1) == QLatin1Char('_')
            || buffer.at(1) == QLatin1Char('t'))) {
        setFormat(currentPos - buffer.length(), buffer.length(), m_formats[ScriptTypeFormat]);
    } else {
        if (isKeyword(buffer))
            setFormat(currentPos - buffer.length(), buffer.length(), m_formats[ScriptKeywordFormat]);
    }
}

QT_END_NAMESPACE

#endif // QT_NO_SYNTAXHIGHLIGHTER

