/*
    SuperCollider Qt IDE
    Copyright (c) 2012 Jakob Leben & Tim Blechmann
    http://www.audiosynth.com

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
*/

#include <cassert>
#include <algorithm>

#include "highlighter.hpp"
#include "../../core/main.hpp"
#include "../../core/settings/manager.hpp"
#include "../../core/settings/theme.hpp"

#include <QApplication>

namespace ScIDE {

SyntaxHighlighterGlobals* SyntaxHighlighterGlobals::mInstance = 0;

SyntaxHighlighterGlobals::SyntaxHighlighterGlobals(Main* main, Settings::Manager* settings): QObject(main) {
    Q_ASSERT(mInstance == 0);
    mInstance = this;

    ScLexer::initLexicalRules();

    // initialize formats from settings:
    applySettings(settings);

    connect(main, SIGNAL(applySettingsRequest(Settings::Manager*)), this, SLOT(applySettings(Settings::Manager*)));
}

void SyntaxHighlighterGlobals::applySettings(Settings::Manager* s) {
    applySettings(s, "whitespace", WhitespaceFormat);
    applySettings(s, "keyword", KeywordFormat);
    applySettings(s, "built-in", BuiltinFormat);
    applySettings(s, "primitive", PrimitiveFormat);
    applySettings(s, "class", ClassFormat);
    applySettings(s, "number", NumberFormat);
    applySettings(s, "symbol", SymbolFormat);
    applySettings(s, "env-var", EnvVarFormat);
    applySettings(s, "string", StringFormat);
    applySettings(s, "char", CharFormat);
    applySettings(s, "comment", CommentFormat);

    Q_EMIT(syntaxFormatsChanged());
}

void SyntaxHighlighterGlobals::applySettings(Settings::Manager* s, const QString& key, SyntaxFormat type) {
    mFormats[type] = s->getThemeVal(key);
}

SyntaxHighlighter::SyntaxHighlighter(QTextDocument* parent): QSyntaxHighlighter(parent) {
    mGlobals = SyntaxHighlighterGlobals::instance();

    connect(mGlobals, SIGNAL(syntaxFormatsChanged()), this, SLOT(rehighlight()));

    connect(Main::scProcess(), &ScProcess::introspectionChanged, this, &SyntaxHighlighter::rehighlight);
}

void SyntaxHighlighter::highlightBlockInCode(ScLexer& lexer) {
    TextBlockData* blockData = static_cast<TextBlockData*>(currentBlockUserData());
    Q_ASSERT(blockData);

    const QTextCharFormat* formats = mGlobals->formats();

    do {
        int tokenPosition = lexer.offset();
        int tokenLength;
        Token::Type tokenType = lexer.nextToken(tokenLength);

        switch (tokenType) {
        case Token::WhiteSpace:
            setFormat(tokenPosition, tokenLength, formats[WhitespaceFormat]);
            break;

        case Token::Class: {
            auto className = QString(lexer.text().begin() + tokenPosition, tokenLength);

            auto* classInstance = Main::scProcess()->introspection().findClass(className);

            if (classInstance != nullptr)
                setFormat(tokenPosition, tokenLength, formats[ClassFormat]);

            break;
        }
        case Token::Builtin:
            setFormat(tokenPosition, tokenLength, formats[BuiltinFormat]);
            break;

        case Token::Primitive:
            setFormat(tokenPosition, tokenLength, formats[PrimitiveFormat]);
            break;

        case Token::Keyword:
            setFormat(tokenPosition, tokenLength, formats[KeywordFormat]);
            break;

        case Token::Symbol:
            setFormat(tokenPosition, tokenLength, formats[SymbolFormat]);
            break;

        case Token::SymbolArg:
            // Don't highlight the trailing ':'
            setFormat(tokenPosition, tokenLength - 1, formats[SymbolFormat]);
            break;

        case Token::EnvVar:
            setFormat(tokenPosition, tokenLength, formats[EnvVarFormat]);
            break;

        case Token::Char:
            setFormat(tokenPosition, tokenLength, formats[CharFormat]);
            break;

        case Token::Float:
        case Token::HexInt:
        case Token::ScaleDegreeFloat:
        case Token::RadixFloat:
            setFormat(tokenPosition, tokenLength, formats[NumberFormat]);
            break;

        case Token::SingleLineComment:
            setFormat(tokenPosition, tokenLength, formats[CommentFormat]);
            break;

        case Token::MultiLineCommentStart:
            setFormat(tokenPosition, tokenLength, formats[CommentFormat]);
            break;

        case Token::StringMark:
            setFormat(tokenPosition, tokenLength, formats[StringFormat]);
            break;

        case Token::SymbolMark:
            setFormat(tokenPosition, tokenLength, formats[SymbolFormat]);
            break;

        default:;
        }

        if ((tokenType != Token::WhiteSpace) && (tokenType != Token::SingleLineComment)
            && (tokenType != Token::MultiLineCommentStart)) {
            Token token(tokenType, tokenPosition, tokenLength);
            if (token.length == 1)
                token.character = lexer.text()[tokenPosition].toLatin1();
            blockData->tokens.push_back(token);
        }

    } while (lexer.state() == ScLexer::InCode && lexer.offset() < lexer.text().size());
}

void SyntaxHighlighter::highlightBlockInString(ScLexer& lexer) {
    int originalOffset = lexer.offset();
    int tokenLength;
    Token::Type tokenType = lexer.nextToken(tokenLength);
    int range = lexer.offset() - originalOffset;
    setFormat(originalOffset, range, mGlobals->format(StringFormat));

    if (tokenType == Token::Unknown)
        return;

    Q_ASSERT(tokenType == Token::StringMark);
    Token token(tokenType, lexer.offset() - 1, 1);
    token.character = '"';

    TextBlockData* blockData = static_cast<TextBlockData*>(currentBlockUserData());
    Q_ASSERT(blockData);
    blockData->tokens.push_back(token);
}

void SyntaxHighlighter::highlightBlockInSymbol(ScLexer& lexer) {
    int originalOffset = lexer.offset();
    int tokenLength;
    Token::Type tokenType = lexer.nextToken(tokenLength);
    int range = lexer.offset() - originalOffset;
    setFormat(originalOffset, range, mGlobals->format(SymbolFormat));

    if (tokenType == Token::Unknown)
        return;

    Q_ASSERT(tokenType == Token::SymbolMark);
    Token token(tokenType, lexer.offset() - 1, 1);
    token.character = '\'';

    TextBlockData* blockData = static_cast<TextBlockData*>(currentBlockUserData());
    Q_ASSERT(blockData);
    blockData->tokens.push_back(token);
}

void SyntaxHighlighter::highlightBlockInComment(ScLexer& lexer) {
    int originalOffset = lexer.offset();
    int tokenLength;
    lexer.nextToken(tokenLength);
    int range = lexer.offset() - originalOffset;
    setFormat(originalOffset, range, mGlobals->format(CommentFormat));
}

void SyntaxHighlighter::highlightBlock(const QString& text) {
    // if we don't have introspection yet don't format anything
    if (!Main::scProcess()->introspection().introspectionAvailable())
        return;

    TextBlockData* blockData = static_cast<TextBlockData*>(currentBlockUserData());
    if (!blockData) {
        blockData = new TextBlockData;
        blockData->tokens.reserve(8);
        setCurrentBlockUserData(blockData);
    } else {
        blockData->tokens.clear();
    }

    int previousState = previousBlockState();
    if (previousState == -1)
        previousState = ScLexer::InCode;

    ScLexer lexer(text, 0, previousState);

    while (lexer.offset() < text.size()) {
        switch (lexer.state()) {
        case ScLexer::InCode:
            highlightBlockInCode(lexer);
            break;

        case ScLexer::InString:
            highlightBlockInString(lexer);
            break;

        case ScLexer::InSymbol:
            highlightBlockInSymbol(lexer);
            break;

        default:
            if (lexer.state() >= ScLexer::InComment)
                highlightBlockInComment(lexer);
        }
    }

    setCurrentBlockState(lexer.state());
}

}
