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

#include "clangdsemantichighlighting.h"

#include "clangdast.h"
#include "clangdclient.h"
#include "clangdqpropertyhighlighter.h"
#include "clangmodelmanagersupport.h"
#include "tasktimers.h"

#include <cppeditor/semantichighlighter.h>
#include <languageclient/languageclientmanager.h>
#include <languageclient/semantichighlightsupport.h>
#include <languageserverprotocol/lsptypes.h>
#include <texteditor/blockrange.h>
#include <texteditor/textstyles.h>

#include <QtConcurrent>
#include <QTextDocument>

#include <new>

using namespace LanguageClient;
using namespace LanguageServerProtocol;
using namespace TextEditor;

namespace ClangCodeModel::Internal {
Q_LOGGING_CATEGORY(clangdLogHighlight, "qtc.clangcodemodel.clangd.highlight", QtWarningMsg);

class ExtraHighlightingResultsCollector
{
public:
    ExtraHighlightingResultsCollector(HighlightingResults &results,
                                      const Utils::FilePath &filePath,
                                      const QTextDocument *doc, const QString &docContent);

    void collect();
private:
    HighlightingResults &m_results;
    const Utils::FilePath m_filePath;
    const QTextDocument * const m_doc;
    const QString &m_docContent;
};

void doSemanticHighlighting(
        QPromise<HighlightingResult> &promise,
        const Utils::FilePath &filePath,
        const QList<ExpandedSemanticToken> &tokens,
        const QString &docContents,
        int docRevision,
        const TaskTimer &taskTimer)
{
    ThreadedSubtaskTimer t("highlighting", taskTimer);
    if (promise.isCanceled())
        return;

    const QTextDocument doc(docContents);

    const std::function<HighlightingResult(const ExpandedSemanticToken &)> toResult
            = [&](const ExpandedSemanticToken &token) {
        TextStyles styles;
        if (token.type == "variable") {
            if (token.modifiers.contains(QLatin1String("functionScope"))) {
                styles.mainStyle = C_LOCAL;
            } else if (token.modifiers.contains(QLatin1String("classScope"))) {
                styles.mainStyle = C_FIELD;
            } else if (token.modifiers.contains(QLatin1String("fileScope"))
                       || token.modifiers.contains(QLatin1String("globalScope"))) {
                styles.mainStyle = C_GLOBAL;
            }
        } else if (token.type == "function" || token.type == "method") {
            styles.mainStyle = token.modifiers.contains(QLatin1String("virtual"))
                    ? C_VIRTUAL_METHOD : C_FUNCTION;
            if (token.modifiers.contains("definition"))
                styles.mixinStyles.push_back(C_FUNCTION_DEFINITION);
        } else if (token.type == "class") {
            styles.mainStyle = C_TYPE;
            if (token.modifiers.contains("constructorOrDestructor"))
                styles.mainStyle = C_FUNCTION;
        } else if (token.type == "comment") { // "comment" means code disabled via the preprocessor
            styles.mainStyle = C_DISABLED_CODE;
        } else if (token.type == "namespace") {
            styles.mainStyle = C_NAMESPACE;
        } else if (token.type == "property") {
            styles.mainStyle = C_FIELD;
        } else if (token.type == "enum") {
            styles.mainStyle = C_TYPE;
        } else if (token.type == "enumMember") {
            styles.mainStyle = C_ENUMERATION;
        } else if (token.type == "parameter") {
            styles.mainStyle = C_PARAMETER;
        } else if (token.type == "macro") {
            styles.mainStyle = C_MACRO;
        } else if (token.type == "type") {
            styles.mainStyle = C_TYPE;
        } else if (token.type == "concept") {
            styles.mainStyle = C_CONCEPT;
        } else if (token.type == "modifier") {
            styles.mainStyle = C_KEYWORD;
        } else if (token.type == "label") {
            styles.mainStyle = C_LABEL;
        } else if (token.type == "typeParameter") {
            // clangd reports both type and non-type template parameters as type parameters,
            // but the latter can be distinguished by the readonly modifier.
            styles.mainStyle = token.modifiers.contains(QLatin1String("readonly"))
                    ? C_PARAMETER : C_TYPE;
        } else if (token.type == "operator") {
            const int pos = Utils::Text::positionInText(&doc, token.line, token.column);
            QTC_ASSERT(pos >= 0 || pos < docContents.size(), return HighlightingResult());
            const QChar firstChar = docContents.at(pos);
            if (firstChar.isLetter())
                styles.mainStyle = C_KEYWORD;
            else
                styles.mainStyle = C_PUNCTUATION;
            styles.mixinStyles.push_back(C_OPERATOR);
            if (token.modifiers.contains("userDefined"))
                styles.mixinStyles.push_back(C_OVERLOADED_OPERATOR);
            else if (token.modifiers.contains("declaration")) {
                styles.mixinStyles.push_back(C_OVERLOADED_OPERATOR);
                styles.mixinStyles.push_back(C_DECLARATION);
            }
            HighlightingResult result(token.line, token.column, token.length, styles);
            if (token.length == 1) {
                if (firstChar == '?')
                    result.kind = CppEditor::SemanticHighlighter::TernaryIf;
                else if (firstChar == ':')
                    result.kind = CppEditor::SemanticHighlighter::TernaryElse;
            }
            return result;
        } else if (token.type == "bracket") {
            styles.mainStyle = C_PUNCTUATION;
            HighlightingResult result(token.line, token.column, token.length, styles);
            const int pos = Utils::Text::positionInText(&doc, token.line, token.column);
            QTC_ASSERT(pos >= 0 || pos < docContents.size(), return HighlightingResult());
            const char symbol = docContents.at(pos).toLatin1();
            QTC_ASSERT(symbol == '<' || symbol == '>', return HighlightingResult());
            result.kind = symbol == '<'
                    ? CppEditor::SemanticHighlighter::AngleBracketOpen
                    : CppEditor::SemanticHighlighter::AngleBracketClose;
            return result;
        }
        if (token.modifiers.contains(QLatin1String("declaration")))
            styles.mixinStyles.push_back(C_DECLARATION);
        if (token.modifiers.contains(QLatin1String("static"))) {
            if (styles.mainStyle == C_FUNCTION) {
                styles.mainStyle = C_STATIC_MEMBER;
                styles.mixinStyles.push_back(C_FUNCTION);
            } else if (styles.mainStyle == C_FIELD) {
                styles.mainStyle = C_STATIC_MEMBER;
            }
        }
        if (token.modifiers.contains(QLatin1String("usedAsMutableReference"))
            || token.modifiers.contains(QLatin1String("usedAsMutablePointer"))) {
            styles.mixinStyles.push_back(C_OUTPUT_ARGUMENT);
        }
        return HighlightingResult(token.line, token.column, token.length, styles);
    };

    const auto safeToResult = [&toResult](const ExpandedSemanticToken &token) {
        try {
            return toResult(token);
        } catch (const std::exception &e) {
            qWarning() << "caught" << e.what() << "in toResult()";
            return HighlightingResult();
        }
    };
    auto results = QtConcurrent::blockingMapped<HighlightingResults>(tokens, safeToResult);
    ExtraHighlightingResultsCollector(results, filePath, &doc, docContents).collect();
    Utils::erase(results, [](const HighlightingResult &res) {
        // QTCREATORBUG-28639
        return res.textStyles.mainStyle == C_TEXT && res.textStyles.mixinStyles.empty();
    });
    if (!promise.isCanceled()) {
        qCInfo(clangdLogHighlight) << "reporting" << results.size() << "highlighting results";
        QList<Range> virtualRanges;
        for (const HighlightingResult &r : results) {
            qCDebug(clangdLogHighlight)
                << '\t' << r.line << r.column << r.length << int(r.textStyles.mainStyle);
            if (r.textStyles.mainStyle != C_VIRTUAL_METHOD)
                continue;
            const Position startPos(r.line - 1, r.column - 1);
            virtualRanges << Range(startPos, startPos.withOffset(r.length, &doc));
        }
        QMetaObject::invokeMethod(LanguageClientManager::instance(),
                                  [filePath, virtualRanges, docRevision] {
            if (ClangdClient * const client = ClangModelManagerSupport::clientForFile(filePath))
                client->setVirtualRanges(filePath, virtualRanges, docRevision);
        }, Qt::QueuedConnection);
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
        promise.addResults(results);
#else
        for (const HighlightingResult &r : results)
            promise.addResult(r);
#endif
    }
}

ExtraHighlightingResultsCollector::ExtraHighlightingResultsCollector(
        HighlightingResults &results,
        const Utils::FilePath &filePath, const QTextDocument *doc,
        const QString &docContent)
    : m_results(results), m_filePath(filePath), m_doc(doc),
      m_docContent(docContent)
{
}

void ExtraHighlightingResultsCollector::collect()
{
    for (int i = 0; i < m_results.length(); ++i) {
        const HighlightingResult res = m_results.at(i);
        if (res.textStyles.mainStyle != TextEditor::C_MACRO || res.length != 10)
            continue;
        const int pos = Utils::Text::positionInText(m_doc, res.line, res.column);
        if (subViewLen(m_docContent, pos, 10) != QLatin1String("Q_PROPERTY"))
            continue;
        int endPos;
        if (i < m_results.length() - 1) {
            const HighlightingResult nextRes = m_results.at(i + 1);
            endPos = Utils::Text::positionInText(m_doc, nextRes.line, nextRes.column);
        } else {
            endPos = m_docContent.length();
        }
        const QString qPropertyString = m_docContent.mid(pos, endPos - pos);
        QPropertyHighlighter propHighlighter(m_doc, qPropertyString, pos);
        for (const HighlightingResult &newRes : propHighlighter.highlight())
            m_results.insert(++i, newRes);
    }
}

class InactiveRegionsParams : public JsonObject
{
public:
    using JsonObject::JsonObject;

    DocumentUri uri() const { return TextDocumentIdentifier(value("textDocument")).uri(); }
    QList<Range> inactiveRegions() const {
        return array<Range>(LanguageServerProtocol::Key{"regions"});
    }
};

class InactiveRegionsNotification : public Notification<InactiveRegionsParams>
{
public:
    explicit InactiveRegionsNotification(const InactiveRegionsParams &params)
        : Notification(inactiveRegionsMethodName(), params) {}
    using Notification::Notification;
};

void handleInactiveRegions(LanguageClient::Client *client, const JsonRpcMessage &msg)
{
    const auto params = InactiveRegionsNotification(msg.toJsonObject()).params();
    if (!params)
        return;
    TextDocument * const doc = client->documentForFilePath(
        params->uri().toFilePath(client->hostPathMapper()));
    if (!doc)
        return;
    const QList<Range> inactiveRegions = params->inactiveRegions();
    QList<BlockRange> ifdefedOutBlocks;
    for (const Range &r : inactiveRegions) {
        const int startPos = r.start().toPositionInDocument(doc->document());
        const int endPos = r.end().toPositionInDocument(doc->document()) + 1;
        ifdefedOutBlocks.emplaceBack(startPos, endPos);
    }
    doc->setIfdefedOutBlocks(ifdefedOutBlocks);
}

QString inactiveRegionsMethodName()
{
    return "textDocument/inactiveRegions";
}

} // namespace ClangCodeModel::Internal
