/*
    SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "lspsemantichighlighting.h"
#include "lspclientprotocol.h"
#include "lspclientservermanager.h"
#include "semantic_tokens_legend.h"

#include <KTextEditor/Document>
#include <KTextEditor/View>

#include <ktexteditor_utils.h>

static KTextEditor::Range getExtendedVisibleRange(KTextEditor::View *view)
{
    auto visibleRange = Utils::getVisibleRange(view);
    if (visibleRange.start().line() > 8) {
        auto s = visibleRange.start();
        s.setLine(s.line() - 8);
        visibleRange.setStart(s);
    }
    if (visibleRange.end().line() + 8 < view->document()->lines()) {
        auto e = visibleRange.end();
        e.setLine(e.line() + 8);
        e.setColumn(view->document()->lineLength(e.line()));
        visibleRange.setEnd(e);
    }
    return visibleRange;
}

SemanticHighlighter::SemanticHighlighter(std::shared_ptr<LSPClientServerManager> serverManager, QObject *parent)
    : QObject(parent)
    , m_serverManager(std::move(serverManager))
{
    m_requestTimer.setSingleShot(true);
    m_requestTimer.connect(&m_requestTimer, &QTimer::timeout, this, [this]() {
        doSemanticHighlighting_impl(m_currentView);
    });
}

void SemanticHighlighter::doSemanticHighlighting(KTextEditor::View *view, bool textChanged)
{
    // start the timer
    // We dont send the request directly because then there can be too many requests
    // which leads to too much load on the server and client/server getting out of sync.
    m_currentView = view;
    if (textChanged) {
        m_requestTimer.start(1000);
    } else {
        // This is not a textChange, its either the user scrolled or view changed etc
        m_requestTimer.start(1);
    }
}

void SemanticHighlighter::doSemanticHighlighting_impl(KTextEditor::View *view)
{
    if (!view) {
        return;
    }

    auto server = m_serverManager->findServer(view);
    if (!server) {
        return;
    }

    const auto &caps = server->capabilities();
    const bool serverSupportsSemHighlighting = caps.semanticTokenProvider.full || caps.semanticTokenProvider.fullDelta || caps.semanticTokenProvider.range;
    if (!serverSupportsSemHighlighting) {
        return;
    }

    auto doc = view->document();
    if (m_docResultId.count(doc) == 0) {
        connect(doc, &KTextEditor::Document::aboutToInvalidateMovingInterfaceContent, this, &SemanticHighlighter::remove, Qt::UniqueConnection);
        connect(doc, &KTextEditor::Document::aboutToDeleteMovingInterfaceContent, this, &SemanticHighlighter::remove, Qt::UniqueConnection);
    }

    disconnect(m_verticalScrollConnection);
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(6, 8, 0)
    m_verticalScrollConnection = connect(view, &KTextEditor::View::displayRangeChanged, this, &SemanticHighlighter::highlightVisibleRange);
#else
    m_verticalScrollConnection = connect(view, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate *)), this, SLOT(highlightVisibleRange()));
#endif

    //  m_semHighlightingManager.setTypes(server->capabilities().semanticTokenProvider.types);

    QPointer<KTextEditor::View> v = view;
    auto h = [this, v, server](const LSPSemanticTokensDelta &st) {
        if (v && server) {
            const auto legend = &server->capabilities().semanticTokenProvider.legend;
            processTokens(st, v, legend);
        }
    };

    if (caps.semanticTokenProvider.range) {
        server->documentSemanticTokensRange(doc->url(), getExtendedVisibleRange(view), this, h);
    } else if (caps.semanticTokenProvider.fullDelta) {
        auto prevResultId = previousResultIdForDoc(doc);
        server->documentSemanticTokensFullDelta(doc->url(), prevResultId, this, h);
    } else {
        server->documentSemanticTokensFull(doc->url(), QString(), this, h);
    }
}

void SemanticHighlighter::highlightVisibleRange()
{
    if (!m_currentView) {
        return;
    }
    auto view = m_currentView.data();
    const auto range = Utils::getVisibleRange(view);
    if (!m_currentHighlightedRange.contains(range)) {
        auto server = m_serverManager->findServer(view);
        if (!server) {
            return;
        }
        if (server->capabilities().semanticTokenProvider.range) {
            // request new range from the server
            doSemanticHighlighting(view, false);
        } else {
            // highlight the newly visible portion
            highlight(view, &server->capabilities().semanticTokenProvider.legend);
        }
    }
}

QString SemanticHighlighter::previousResultIdForDoc(KTextEditor::Document *doc) const
{
    auto it = m_docResultId.find(doc);
    if (it != m_docResultId.end()) {
        return it->second;
    }
    return QString();
}

void SemanticHighlighter::processTokens(const LSPSemanticTokensDelta &tokens, KTextEditor::View *view, const SemanticTokensLegend *legend)
{
    Q_ASSERT(view);

    for (const auto &semTokenEdit : tokens.edits) {
        update(view->document(), tokens.resultId, semTokenEdit.start, semTokenEdit.deleteCount, semTokenEdit.data);
    }

    if (!tokens.data.empty()) {
        insert(view->document(), tokens.resultId, tokens.data);
    }
    highlight(view, legend);
}

void SemanticHighlighter::remove(KTextEditor::Document *doc)
{
    m_docResultId.erase(doc);
    m_docSemanticInfo.erase(doc);
}

void SemanticHighlighter::insert(KTextEditor::Document *doc, const QString &resultId, const std::vector<uint32_t> &data)
{
    m_docResultId[doc] = resultId;
    TokensData &tokensData = m_docSemanticInfo[doc];
    tokensData.tokens = data;
}

/**
 * Handle semantic tokens edits
 */
void SemanticHighlighter::update(KTextEditor::Document *doc, const QString &resultId, uint32_t start, uint32_t deleteCount, const std::vector<uint32_t> &data)
{
    auto toks = m_docSemanticInfo.find(doc);
    if (toks == m_docSemanticInfo.end()) {
        return;
    }

    auto &existingTokens = toks->second.tokens;

    // replace
    if (deleteCount > 0) {
        existingTokens.erase(existingTokens.begin() + start, existingTokens.begin() + start + deleteCount);
    }
    existingTokens.insert(existingTokens.begin() + start, data.begin(), data.end());

    //     Update result Id
    m_docResultId[doc] = resultId;
}

void SemanticHighlighter::highlight(KTextEditor::View *view, const SemanticTokensLegend *legend)
{
    Q_ASSERT(legend);

    if (!view || !legend) {
        return;
    }
    auto doc = view->document();
    TokensData &semanticData = m_docSemanticInfo[doc];
    auto &movingRanges = semanticData.movingRanges;
    auto &data = semanticData.tokens;

    if (data.size() % 5 != 0) {
        qWarning() << "Bad data for doc: " << doc->url() << " skipping";
        return;
    }

    auto visibleRange = getExtendedVisibleRange(view);
    m_currentHighlightedRange = visibleRange;

    uint32_t currentLine = 0;
    uint32_t start = 0;
    auto oldRanges = std::move(movingRanges);

    for (size_t i = 0; i < data.size(); i += 5) {
        const uint32_t deltaLine = data[i];

        currentLine += deltaLine;

        // we only highlight currently visible lines
        if (!visibleRange.overlapsLine(currentLine)) {
            if (visibleRange.end().line() < (int)currentLine) {
                break;
            }
            continue;
        }

        const uint32_t deltaStart = data[i + 1];
        const uint32_t len = data[i + 2];
        const uint32_t type = data[i + 3];
        // auto mod = data[i + 4];

        if (deltaLine == 0) {
            start += deltaStart;
        } else {
            start = deltaStart;
        }

        // QString text = doc->line(currentLine);
        // text = text.mid(start, len);

        auto attribute = legend->attributeForTokenType(type);
        if (!attribute) {
            continue;
        }

        KTextEditor::Range r(currentLine, start, currentLine, start + len);
        using MovingRangePtr = std::unique_ptr<KTextEditor::MovingRange>;
        // Check if we have a moving range for 'r' already available
        auto it = std::lower_bound(oldRanges.begin(), oldRanges.end(), r, [](const MovingRangePtr &mr, KTextEditor::Range r) {
            // null range is considered less
            // it can be null because we 'move' the range from oldRanges whenever we find a matching
            return !mr || mr->toRange() < r;
        });

        MovingRangePtr range;
        if (it != oldRanges.end() && (*it) && (*it)->toRange() == r && (*it)->attribute() == attribute.constData()) {
            range = std::move(*it);
        } else {
            range.reset(doc->newMovingRange(r));
            range->setZDepth(-91000.0);
            range->setRange(r);
            range->setAttribute(std::move(attribute));
        }
        movingRanges.push_back(std::move(range));
    }
}

#include "moc_lspsemantichighlighting.cpp"
