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

#include "codehelper.h"
#include "effectmanager.h"
#include "syntaxhighlighterdata.h"
#include <QString>

CodeHelper::CodeHelper(QObject *parent)
    : QObject{parent}
{
    m_effectManager = static_cast<EffectManager *>(parent);
    m_codeCompletionModel = new CodeCompletionModel(this);
    m_codeCompletionTimer.setInterval(500);
    m_codeCompletionTimer.setSingleShot(true);
    connect(&m_codeCompletionTimer, &QTimer::timeout, this, &CodeHelper::showCodeCompletion);

    // Cache syntax highlight data
    auto args = SyntaxHighlighterData::reservedArgumentNames();
    for (const auto &arg : args)
        m_reservedArgumentNames << QString::fromUtf8(arg);
    auto funcs = SyntaxHighlighterData::reservedFunctionNames();
    for (const auto &func : funcs)
        m_reservedFunctionNames << QString::fromUtf8(func);
    auto tags = SyntaxHighlighterData::reservedTagNames();
    for (const auto &tag : tags)
        m_reservedTagNames << QString::fromUtf8(tag);

    std::sort(m_reservedArgumentNames.begin(), m_reservedArgumentNames.end());
    std::sort(m_reservedFunctionNames.begin(), m_reservedFunctionNames.end());
    std::sort(m_reservedTagNames.begin(), m_reservedTagNames.end());
}

// Process the keyCode and return true if key was handled and
// TextEdit itself shouldn't append it.
bool CodeHelper::processKey(QQuickTextEdit *textEdit, int keyCode, int modifiers)
{
    int position = textEdit->cursorPosition();
    const QString &code = textEdit->text();
    if (m_codeCompletionVisible) {
        // When code completition is visible, some keys have special meening
        if (keyCode == Qt::Key_Escape || keyCode == Qt::Key_Left || keyCode == Qt::Key_Right) {
            setCodeCompletionVisible(false);
            return true;
        } else if (keyCode == Qt::Key_Tab || keyCode == Qt::Key_Return) {
            applyCodeCompletion(textEdit);
            return true;
        } else if (keyCode == Qt::Key_Down) {
            m_codeCompletionModel->nextItem();
            return true;
        } else if (keyCode == Qt::Key_Up) {
            m_codeCompletionModel->previousItem();
            return true;
        } else {
            updateCodeCompletion(textEdit);
        }
    }
    if (keyCode == Qt::Key_Return) {
        // See if we are inside multiline comments
        bool multilineComment = false;
        QString prevCode = code.left(position);
        int codeStartPos = prevCode.lastIndexOf("/*");
        int codeEndPos = prevCode.lastIndexOf("*/");
        if (codeStartPos > codeEndPos)
            multilineComment = true;

        // Get only the previous line before pressing return
        QString singleLine = code.left(position);
        int lineStartPos = singleLine.lastIndexOf('\n');
        if (lineStartPos > -1)
            singleLine = singleLine.remove(0, lineStartPos + 1);

        QString indent = autoIndentGLSLNextLine(singleLine, multilineComment);
        textEdit->insert(position, indent);
        return true;
    } else if (keyCode == Qt::Key_BraceRight) {
        // Get only the current line, without the last '}'
        QString singleLine = code.left(position);
        int lineStartPos = singleLine.lastIndexOf('\n');
        if (lineStartPos > -1)
            singleLine = singleLine.remove(0, lineStartPos + 1);

        if (singleLine.trimmed().isEmpty()) {
            // Line only had '}' so unindent it (max) one step
            int startPos = std::max(int(position - singleLine.size()), position - 4);
            textEdit->remove(startPos, position);
        }
        return false;
    } else if (keyCode == Qt::Key_Tab) {
        // Replace tabs with spaces
        QString indent = QStringLiteral("    ");
        textEdit->insert(position, indent);
        return true;
    } else if ((modifiers & Qt::ControlModifier) && keyCode == Qt::Key_Space) {
        updateCodeCompletion(textEdit, true);
    }
    return false;
}


// Formats GLSL code with simple indent rules.
// Not a full parser so doesn't work with more complex code.
// For that, consider ClangFormat etc.
QString CodeHelper::autoIndentGLSLCode(const QString &code)
{
    QStringList out;
    // Index (indent level) currently
    int index = 0;
    // Index (indent level) for previous line
    int prevIndex = 0;
    // Indent spaces
    QString indent;
    // True when we are inside multiline comments (/* .. */)
    bool multilineComment = false;
    // Increased when command continues to next line (e.g. "if ()")
    int nextLineIndents = 0;
    // Stores indent before nextLineIndents started
    int indentBeforeSingles = 0;

    QStringList codeList = code.split('\n');
    for (const auto &cOrig : codeList) {
        QString c = cOrig.trimmed();

        if (c.isEmpty()) {
            // Lines with only spaces are emptied but kept
            out << c;
            continue;
        }

        bool isPreprocessor = c.startsWith('#');
        if (isPreprocessor) {
            // Preprocesser lines have zero intent
            out << c;
            continue;
        }

        bool isTag = c.startsWith('@');

        // Separate content after "//" to commenPart
        QString commentPart;
        int lineCommentIndex = c.indexOf("//");
        if (lineCommentIndex > -1) {
            commentPart = c.last(c.length() - lineCommentIndex);
            c = c.first(lineCommentIndex);
            // Move spaces from c to comment part (to also trim c)
            while (!c.isEmpty() && c.endsWith(' ')) {
                commentPart.prepend(' ');
                c.chop(1);
            }
        }

        // Multiline comments state
        int startComments = c.count("/*");
        int endComments = c.count("*/");
        int commendsChange = startComments - endComments;
        if (commendsChange > 0)
            multilineComment = true;
        else if (commendsChange < 0)
            multilineComment = false;

        if (multilineComment || endComments > startComments) {
            // Lines inside /* .. */ are not touched
            out << cOrig;
            continue;
        }

        // Check indent for next line
        int indexChange = 0;
        int startBlocks = c.count('{');
        int endBlocks = c.count('}');
        indexChange += startBlocks - endBlocks;
        int startBrackets = c.count('(');
        int endBrackets = c.count(')');
        indexChange += startBrackets - endBrackets;
        index += indexChange;
        index = std::max(index, 0);
        indent.clear();
        int currentIndex = indexChange > 0 ? prevIndex : index;
        if (!isTag) {
            if (currentIndex > 0 && startBlocks > 0 && endBlocks > 0) {
                // Note: "} else {", "} else if () {"
                // Indent one step lower
                currentIndex--;
            } else if (endBrackets > startBrackets) {
                // Note: "variable)"
                // Indent one step higher
                currentIndex++;
            }
            if (!c.startsWith('{'))
                currentIndex += nextLineIndents;

            if (!c.isEmpty() && startBlocks == 0 && endBlocks == 0 && indexChange == 0 && !c.endsWith(';') && !c.endsWith(',') && !c.endsWith('(') && !c.endsWith("*/")) {
                // Note: "if ()", "else if ()", "else", "for (..)"
                // Something that should continue to next line
                nextLineIndents++;
            } else if (nextLineIndents > 0) {
                // Return to indent before e.g. "if (thing) \n if (thing2) \n something;"
                nextLineIndents = indentBeforeSingles;
                indentBeforeSingles = nextLineIndents;
            }
        }

        // Apply indent
        QString singleIndentStep = QStringLiteral("    ");
        for (int i = 0; i < currentIndex; i++)
            indent += singleIndentStep;
        c.prepend(indent);

        out << (c + commentPart);
        prevIndex = index;
    }
    return out.join('\n');
}

// Takes in the previous line (before pressing return key)
// and returns suitable indent string with spaces.
QString CodeHelper::autoIndentGLSLNextLine(const QString &codeLine, bool multilineComment)
{
    int spaces = 0;

    // Check how many spaces previous line had
    for (int i = 0; i < codeLine.size() ; i++) {
        if (codeLine.at(i) == ' ')
            spaces++;
        else
            break;
    }

    QString c = codeLine.trimmed();
    bool isPreprocessor = c.startsWith('#');
    bool isTag = c.startsWith('@');

    // Remove content after "//"
    int lineCommentIndex = c.indexOf("//");
    if (lineCommentIndex > -1) {
        c = c.first(lineCommentIndex);
        c = c.trimmed();
    }

    if (!c.isEmpty() && !isPreprocessor && !isTag && !multilineComment) {
        // Check indent for next line
        int index = 0;
        int indexChange = 0;
        int startBlocks = c.count('{');
        int endBlocks = c.count('}');
        indexChange += startBlocks - endBlocks;
        int startBrackets = c.count('(');
        int endBrackets = c.count(')');
        indexChange += startBrackets - endBrackets;
        if (indexChange > 0) {
            // Note: "{" "if () {", "vec4 x = vec4("
            index++;
        } else if (indexChange < 0 && c.trimmed().size() > 1) {
            // Note: "something; }", but NOT "}" as it has be unindented already
            index--;
        }

        if (index == 0 && !c.isEmpty() && !c.endsWith(';') && !c.endsWith("*/") && !c.endsWith('}')) {
            // Note: "if ()", "else if ()", "else", "for (..)"
            // Something that should continue to next line
            index++;
        }
        spaces += 4 * index;
    }

    QString indent;
    indent.fill(' ', spaces);
    return '\n' + indent;
}

// Get current word under the cursor
// Word is letters, digits and "_".
// Also if word starts with "//" that is included
QString CodeHelper::getCurrentWord(QQuickTextEdit *textEdit)
{
    if (!textEdit)
        return QString();
    int cursorPosition = textEdit->cursorPosition();
    int cPos = cursorPosition - 1;
    int maxPos = textEdit->text().size();
    QString currentWord;
    QChar c = textEdit->getText(cPos, cPos + 1).front();
    while (cPos >= 0 && (c.isLetterOrNumber() || c == '_')) {
        currentWord.prepend(c);
        cPos--;
        c = textEdit->getText(cPos, cPos + 1).front();
    }
    // Special case of "@" tags
    if (cPos >= 1) {
        QString s = textEdit->getText(cPos, cPos + 1);
        if (s == QStringLiteral("@"))
            currentWord.prepend(QStringLiteral("@"));
    }
    cPos = cursorPosition;
    c = textEdit->getText(cPos, cPos + 1).front();
    while (cPos <= maxPos && (c.isLetterOrNumber() || c == '_')) {
        currentWord.append(c);
        cPos++;
        c = textEdit->getText(cPos, cPos + 1).front();
    }
    return currentWord;
}

// Remove the current word under cursor
void CodeHelper::removeCurrentWord(QQuickTextEdit *textEdit)
{
    if (!textEdit)
        return;
    int cursorPosition = textEdit->cursorPosition();
    int cPos = cursorPosition - 1;
    int maxPos = textEdit->text().size();
    int firstPos = 0;
    int lastPos = 0;
    QChar c = textEdit->getText(cPos, cPos + 1).front();
    while (cPos >= 0 && (c.isLetterOrNumber() || c == '_')) {
        cPos--;
        c = textEdit->getText(cPos, cPos + 1).front();
    }
    // Special case of "@" tags
    if (cPos >= 1) {
        QString s = textEdit->getText(cPos, cPos + 1);
        if (s == QStringLiteral("@"))
            cPos--;
    }
    firstPos = cPos + 1;
    cPos = cursorPosition;
    c = textEdit->getText(cPos, cPos + 1).front();
    while (cPos <= maxPos && (c.isLetterOrNumber() || c == '_')) {
        cPos++;
        c = textEdit->getText(cPos, cPos + 1).front();
    }
    lastPos = cPos;
    textEdit->remove(firstPos, lastPos);
}

bool CodeHelper::codeCompletionVisible() const
{
    return m_codeCompletionVisible;
}

void CodeHelper::setCodeCompletionVisible(bool visible)
{
    if (m_codeCompletionVisible == visible)
        return;

    m_codeCompletionVisible = visible;
    if (!m_codeCompletionVisible)
        m_codeCompletionModel->setCurrentIndex(0);

    Q_EMIT codeCompletionVisibleChanged();
}

// Update and show code completion popup
// When force is true, do this without a delay.
void CodeHelper::updateCodeCompletion(QQuickTextEdit *textEdit, bool force)
{
    m_textEdit = textEdit;
    if (force)
        showCodeCompletion();
    else
        m_codeCompletionTimer.start();
}

void CodeHelper::showCodeCompletion()
{
    if (!m_textEdit)
        return;

    QString currentWord = getCurrentWord(m_textEdit);
    QString currentWordCleaned = currentWord;
    bool isComment = false;

    // Check if word is comment/tag
    if (currentWordCleaned.size() >= 2 && currentWordCleaned.left(2) == "//") {
        isComment = true;
        currentWordCleaned = currentWordCleaned.right(currentWordCleaned.size() - 2);
    }

    m_codeCompletionModel->beginResetModel();
    m_codeCompletionModel->clearItems();

    if (!isComment) {
        for (const auto &a : m_reservedArgumentNames) {
            if (a.startsWith(currentWordCleaned, Qt::CaseInsensitive) && a.size() > currentWordCleaned.size())
                m_codeCompletionModel->addItem(a, CodeCompletionModel::TypeArgument);
        }

        for (const auto &a : m_reservedFunctionNames) {
            if (a.startsWith(currentWordCleaned, Qt::CaseInsensitive) && a.size() > currentWordCleaned.size())
                m_codeCompletionModel->addItem(a, CodeCompletionModel::TypeFunction);
        }

        for (const auto &a : m_reservedTagNames) {
            if (a.startsWith(currentWord, Qt::CaseInsensitive) && a.size() > currentWord.size())
                m_codeCompletionModel->addItem(a, CodeCompletionModel::TypeTag);
        }
    }

    m_codeCompletionModel->endResetModel();

    if (!m_codeCompletionModel->m_modelList.isEmpty()) {
        m_codeCompletionModel->setCurrentIndex(0);
        setCodeCompletionVisible(true);
    } else {
        setCodeCompletionVisible(false);
    }
}

CodeCompletionModel *CodeHelper::codeCompletionModel() const
{
    return m_codeCompletionModel;
}

void CodeHelper::applyCodeCompletion(QQuickTextEdit *textEdit)
{
    CodeCompletionModel::ModelData t = m_codeCompletionModel->currentItem();
    if (!t.name.isEmpty()) {
        // Replace the current word with code completion one
        removeCurrentWord(textEdit);
        textEdit->insert(textEdit->cursorPosition(), t.name);
        if (t.type == CodeCompletionModel::TypeFunction) {
            // For functions, place cursor between parentheses, e.g. "sin(|)".
            textEdit->setCursorPosition(textEdit->cursorPosition() - 1);
        }
    }
    setCodeCompletionVisible(false);
}
