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

#include "syntaxhighlighterrunner.h"

#include "fontsettings.h"
#include "textdocumentlayout.h"
#include "texteditorsettings.h"
#include "highlighter.h"

#include <utils/algorithm.h>
#include <utils/textutils.h>

#include <QMetaObject>
#include <QTextCursor>
#include <QTextDocument>
#include <QThread>

namespace TextEditor {

struct BlockPreeditData {
    int position;
    QString text;
};

class SyntaxHighlighterRunnerPrivate : public QObject
{
    Q_OBJECT
public:
    SyntaxHighlighterRunnerPrivate(SyntaxHighlighter *highlighter,
                                   QTextDocument *document,
                                   bool async)
        : m_highlighter(highlighter)
    {
        if (async) {
            m_document = new QTextDocument(this);
            m_document->setDocumentLayout(new TextDocumentLayout(m_document));
        } else {
            m_document = document;
        }

        m_highlighter->setParent(m_document);
        m_highlighter->setDocument(m_document);

        connect(m_highlighter,
                &SyntaxHighlighter::resultsReady,
                this,
                &SyntaxHighlighterRunnerPrivate::resultsReady);
    }

    void changeDocument(int from,
                        int charsRemoved,
                        const QString textAdded,
                        const QMap<int, BlockPreeditData> &blocksPreedit)
    {
        QTextCursor cursor(m_document);
        cursor.setPosition(qMin(m_document->characterCount() - 1, from + charsRemoved));
        cursor.setPosition(from, QTextCursor::KeepAnchor);
        cursor.insertText(textAdded);

        for (auto it = blocksPreedit.cbegin(); it != blocksPreedit.cend(); ++it) {
            const QTextBlock block = m_document->findBlockByNumber(it.key());
            block.layout()->setPreeditArea(it.value().position, it.value().text);
        }
    }

    void setExtraFormats(const QMap<int, QList<QTextLayout::FormatRange>> &formatMap)
    {
        QTC_ASSERT(m_highlighter, return);
        for (auto it = formatMap.cbegin(); it != formatMap.cend(); ++it)
            m_highlighter->setExtraFormats(m_document->findBlockByNumber(it.key()), it.value());
    }

    void clearExtraFormats(const QList<int> &blockNumbers)
    {
        QTC_ASSERT(m_highlighter, return);
        for (auto it = blockNumbers.cbegin(); it != blockNumbers.cend(); ++it)
            m_highlighter->clearExtraFormats(m_document->findBlockByNumber(*it));
    }

    void clearAllExtraFormats()
    {
        QTC_ASSERT(m_highlighter, return);
        m_highlighter->clearAllExtraFormats();
    }

    void setFontSettings(const TextEditor::FontSettings &fontSettings)
    {
        QTC_ASSERT(m_highlighter, return);
        m_highlighter->setFontSettings(fontSettings);
    }

    void setDefinitionName(const QString &name)
    {
        QTC_ASSERT(m_highlighter, return);
        m_highlighter->setDefinitionName(name);
    }

    void setLanguageFeaturesFlags(unsigned int flags)
    {
        QTC_ASSERT(m_highlighter, return);
        m_highlighter->setLanguageFeaturesFlags(flags);
    }

    void setEnabled(bool enabled)
    {
        QTC_ASSERT(m_highlighter, return);
        m_highlighter->setEnabled(enabled);
    }

    void rehighlight()
    {
        QTC_ASSERT(m_highlighter, return);
        m_highlighter->rehighlight();
    }

    void reformatBlocks(int from, int charsRemoved, int charsAdded)
    {
        QTC_ASSERT(m_highlighter, return);
        m_highlighter->reformatBlocks(from, charsRemoved, charsAdded);
    }

    void setInterrupted(bool interrupted)
    {
        QTC_ASSERT(m_highlighter, return);
        m_highlighter->setInterrupted(interrupted);
    }

    QPointer<SyntaxHighlighter> m_highlighter = nullptr;
    QTextDocument *m_document = nullptr;

signals:
    void resultsReady(const QList<SyntaxHighlighter::Result> &result);

};

void SyntaxHighlighterRunner::HighlightingStatus::notInterrupted(int from,
                                                                 int charsRemoved,
                                                                 int charsAdded)
{
    m_from = from;
    m_addedChars = charsAdded;
    m_removedChars = charsRemoved;
    m_current = from;
    m_newFrom = from + m_addedChars;
    m_interruptionRequested = false;
}

void SyntaxHighlighterRunner::HighlightingStatus::interrupted(int from,
                                                              int charsRemoved,
                                                              int charsAdded)
{
    m_newFrom = std::min(m_newFrom, from);
    m_newFrom = std::min(m_current, m_newFrom);
    m_removedChars += charsRemoved;
    m_addedChars += charsAdded;
    m_interruptionRequested = true;
}

void SyntaxHighlighterRunner::HighlightingStatus::applyNewFrom()
{
    m_from = m_newFrom;
    m_current = m_newFrom;
    m_interruptionRequested = false;
}

SyntaxHighlighterRunner::SyntaxHighlighterRunner(SyntaxHighlighter *highlighter,
                                                 QTextDocument *document,
                                                 bool async)
    : d(new SyntaxHighlighterRunnerPrivate(highlighter, document, async))
    , m_document(document)
{
    m_useGenericHighlighter = qobject_cast<Highlighter *>(d->m_highlighter);

    if (async) {
        m_thread.emplace();
        d->moveToThread(&*m_thread);
        connect(&*m_thread, &QThread::finished, d, &QObject::deleteLater);
        m_thread->start();

        connect(d,
                &SyntaxHighlighterRunnerPrivate::resultsReady,
                this,
                &SyntaxHighlighterRunner::applyFormatRanges);

        changeDocument(0, 0, m_document->characterCount());
        connect(m_document,
                &QTextDocument::contentsChange,
                this,
                &SyntaxHighlighterRunner::changeDocument);

        m_foldValidator.setup(qobject_cast<TextDocumentLayout *>(document->documentLayout()));
    } else {
        connect(d,
                &SyntaxHighlighterRunnerPrivate::resultsReady,
                this,
                [this](const QList<SyntaxHighlighter::Result> &result) {
                    if (result.size() == 1
                        && result.at(0).m_state == SyntaxHighlighter::State::Extras)
                        return;

                    auto done = std::find_if(result.cbegin(),
                                             result.cend(),
                                             [](const SyntaxHighlighter::Result &res) {
                                                 return res.m_state
                                                        == SyntaxHighlighter::State::Done;
                                             });
                    if (done != result.cend()) {
                        m_syntaxInfoUpdated = SyntaxHighlighter::State::Done;
                        emit highlightingFinished();
                        return;
                    }
                    m_syntaxInfoUpdated = SyntaxHighlighter::State::InProgress;
                });
    }
}

SyntaxHighlighterRunner::~SyntaxHighlighterRunner()
{
    if (m_thread) {
        m_thread->requestInterruption();
        m_thread->quit();
        m_thread->wait();
    } else {
        delete d;
    }
}

void SyntaxHighlighterRunner::applyFormatRanges(const QList<SyntaxHighlighter::Result> &results)
{
    if (m_document == nullptr)
        return;

    if (m_highlightingStatus.m_interruptionRequested) {
        d->setInterrupted(false);
        m_highlightingStatus.applyNewFrom();
        reformatBlocks(m_highlightingStatus.m_newFrom,
                       m_highlightingStatus.m_removedChars,
                       m_highlightingStatus.m_addedChars);
        return;
    }

    auto processResult = [this](SyntaxHighlighter::Result result, QTextBlock docBlock) {
        if (!docBlock.isValid())
            return;

        result.copyToBlock(docBlock);
        m_highlightingStatus.m_current = docBlock.position() + docBlock.length() - 1;

        if (result.m_formatRanges != docBlock.layout()->formats()) {
            docBlock.layout()->setFormats(result.m_formatRanges);
            m_document->markContentsDirty(docBlock.position(), docBlock.length());
        }
    };

    if (results.size() == 1 && results.at(0).m_state == SyntaxHighlighter::State::Extras) {
        QTextBlock docBlock = m_document->findBlockByNumber(results.at(0).m_blockNumber);
        processResult(results.at(0), docBlock);
        return;
    }

    for (const SyntaxHighlighter::Result &result : results) {
        m_syntaxInfoUpdated = result.m_state;
        if (m_syntaxInfoUpdated == SyntaxHighlighter::State::Start) {
            m_foldValidator.reset();
            continue;
        }
        if (m_syntaxInfoUpdated == SyntaxHighlighter::State::Done) {
            m_foldValidator.finalize();
            emit highlightingFinished();
            return;
        }

        QTextBlock docBlock = m_document->findBlockByNumber(result.m_blockNumber);
        processResult(result, docBlock);
        m_foldValidator.process(docBlock);
    }
}

void SyntaxHighlighterRunner::changeDocument(int from, int charsRemoved, int charsAdded)
{
    QTC_ASSERT(m_document, return);
    SyntaxHighlighter::State prevSyntaxInfoUpdated = m_syntaxInfoUpdated;
    m_syntaxInfoUpdated = SyntaxHighlighter::State::InProgress;

    QMap<int, BlockPreeditData> blocksPreedit;
    QTextBlock block = m_document->findBlock(from);
    const QTextBlock endBlock = m_document->findBlock(from + charsAdded);
    while (block.isValid() && block != endBlock) {
        if (QTextLayout *layout = block.layout()) {
            if (const int pos = layout->preeditAreaPosition(); pos != -1)
                blocksPreedit[block.blockNumber()] = {pos, layout->preeditAreaText()};
        }
        block = block.next();
    }
    const QString text = Utils::Text::textAt(QTextCursor(m_document), from, charsAdded);
    QMetaObject::invokeMethod(d, [this, from, charsRemoved, text, blocksPreedit] {
        d->changeDocument(from, charsRemoved, text, blocksPreedit);
    });

    if (prevSyntaxInfoUpdated == SyntaxHighlighter::State::InProgress) {
        m_highlightingStatus.interrupted(from, charsRemoved, charsAdded);
        d->setInterrupted(true);
    } else {
        m_highlightingStatus.notInterrupted(from, charsRemoved, charsAdded);
        d->setInterrupted(false);
    }
}

bool SyntaxHighlighterRunner::useGenericHighlighter() const
{
    return m_useGenericHighlighter;
}

void SyntaxHighlighterRunner::setExtraFormats(
    const QMap<int, QList<QTextLayout::FormatRange>> &formatMap)
{
    QMetaObject::invokeMethod(d, [this, formatMap] { d->setExtraFormats(formatMap); });
}

void SyntaxHighlighterRunner::clearExtraFormats(const QList<int> &blockNumbers)
{
    QMetaObject::invokeMethod(d, [this, blockNumbers] { d->clearExtraFormats(blockNumbers); });
}

void SyntaxHighlighterRunner::clearAllExtraFormats()
{
    QMetaObject::invokeMethod(d, [this] { d->clearAllExtraFormats(); });
}

void SyntaxHighlighterRunner::setFontSettings(const TextEditor::FontSettings &fontSettings)
{
    QMetaObject::invokeMethod(d, [this, fontSettings] { d->setFontSettings(fontSettings); });
    rehighlight();
}

void SyntaxHighlighterRunner::setLanguageFeaturesFlags(unsigned int flags)
{
    QMetaObject::invokeMethod(d, [this, flags] { d->setLanguageFeaturesFlags(flags); });
}

void SyntaxHighlighterRunner::setEnabled(bool enabled)
{
    QMetaObject::invokeMethod(d, [this, enabled] { d->setEnabled(enabled); });
}

void SyntaxHighlighterRunner::rehighlight()
{
    if (m_syntaxInfoUpdated == SyntaxHighlighter::State::InProgress) {
        m_highlightingStatus.interrupted(0, 0, m_document->characterCount());
        d->setInterrupted(true);
    } else {
        m_highlightingStatus.notInterrupted(0, 0, m_document->characterCount());
        d->setInterrupted(false);
        QMetaObject::invokeMethod(d, [this] { d->rehighlight(); });
    }
}


void SyntaxHighlighterRunner::reformatBlocks(int from, int charsRemoved, int charsAdded)
{
    QMetaObject::invokeMethod(
        d,
        [this, from, charsRemoved, charsAdded] {
            d->reformatBlocks(from, charsRemoved, charsAdded);
        });
}

QString SyntaxHighlighterRunner::definitionName()
{
    return m_definitionName;
}

void SyntaxHighlighterRunner::setDefinitionName(const QString &name)
{
    if (name.isEmpty())
        return;

    m_definitionName = name;
    QMetaObject::invokeMethod(d, [this, name] { d->setDefinitionName(name); });
}

} // namespace TextEditor

#include "syntaxhighlighterrunner.moc"
