/*
    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
*/

#define QT_NO_DEBUG_OUTPUT

#include "autocompleter.hpp"
#include "sc_editor.hpp"
#include "tokens.hpp"
#include "../../core/sc_introspection.hpp"
#include "../../core/sc_process.hpp"
#include "../../core/main.hpp"
#include "../../core/util/standard_dirs.hpp"
#include "../main_window.hpp"

#ifdef SC_USE_QTWEBENGINE
#    include "../help_browser.hpp"
#endif // SC_USE_QTWEBENGINE

#include <yaml-cpp/node/node.h>
#include <yaml-cpp/parser.h>

#include <QDebug>
#include <QLabel>
#include <QScrollBar>
#include <QApplication>
#include <QScreen>
#include <QWindow>
#include <QProxyStyle>
#include <QFile>

namespace ScIDE {

static bool tokenMaybeName(Token::Type type) {
    return (type == Token::Name || type == Token::Keyword || type == Token::Builtin);
}

static QString incrementedString(const QString& other) {
    if (other.isEmpty())
        return QString();

    QString str = other;
    int pos = str.length() - 1;
    str[pos] = QChar(str[pos].unicode() + 1);
    return str;
}

class MethodCallWidget : public QWidget {
public:
    MethodCallWidget(QWidget* parent = 0): QWidget(parent, Qt::ToolTip) {
        mLabel = new QLabel(this);
        mLabel->setTextFormat(Qt::RichText);
        mLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);

        // Qt 4 had a class called "QGtkStyle" which allowed Qt to access the
        // native style of the window manager in Linux and other *nix systems.
        // It was removed in Qt 5, so we don't have that option anymore. We have
        // to hardcode the colors now. In the future, this should be configurable
        // by the user.

        // This #if used to be "#if defined(Q_WS_X11)", and then NetBSD was
        // excluded (commit c3017f5) because QGtkStyle was not defined on that
        // system. Qt 5 got rid of the Q_WS_ macros, so we changed Q_WS_X11 to
        // Q_OS_UNIX && !Q_OS_MAC as a best guess. The NetBSD check is probably
        // vestigial, but we REALLY needed to get this fix through since Linux
        // users have had unreadable autocomplete widgets for almost 18 months
        // now. We figured it was best to leave it alone.

        // See: https://github.com/supercollider/supercollider/pull/2762

#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(__NetBSD__)
        QPalette p;
        p.setColor(QPalette::Window, QColor(255, 255, 220));
        p.setColor(QPalette::WindowText, Qt::black);
        setPalette(p);
#else
        QPalette p(palette());
        p.setColor(QPalette::Window, p.color(QPalette::ToolTipBase));
        setPalette(p);
        mLabel->setForegroundRole(QPalette::ToolTipText);
#endif
    }

    void showMethod(const AutoCompleter::MethodCall& methodCall, int argNum, const QRect& cursorRect) {
        const ScLanguage::Method* method = methodCall.method;
        int argc = method->arguments.count();

        QString text;

        if (methodCall.functionalNotation) {
            addArgument(text, "receiver", QString(), argNum == 0);
            --argNum;
            if (argc)
                text += ", &nbsp;&nbsp;";
        }

        for (int i = 0; i < argc; ++i) {
            const ScLanguage::Argument& arg = method->arguments[i];

            addArgument(text, arg.name, arg.defaultValue, argNum == i);

            if (i != argc - 1)
                text += ", &nbsp;&nbsp;";
        }

        mLabel->setText(text);

        mTargetRect = cursorRect;

        updateGeometry();
        show();
    }

private:
    void static addArgument(QString& text, const QString& argText, const QString& valText, bool highlight) {
        if (highlight) {
            text += QString("<span style=\""
                            //"text-decoration: underline;"
                            "font-weight: bold;"
                            "\">");
        }

        text += argText;

        if (!valText.isEmpty())
            text += " = " + valText;

        if (highlight)
            text += "</span>";
    }

    void updateGeometry() {
        static const QSize margins = QSize(5, 2);

        QSize labelSize = mLabel->sizeHint();
        mLabel->move(margins.width(), margins.height());
        mLabel->resize(labelSize);

        QRect rect;
        rect.setSize(labelSize + (margins * 2));
        rect.moveBottomLeft(mTargetRect.topLeft());

        QWidget* parentWid = parentWidget();
        QWidget* referenceWidget = parentWid ? parentWid : this;

        QRect screen = referenceWidget->screen()->availableGeometry();
        if (!screen.contains(rect)) {
            if (rect.right() > screen.right())
                rect.moveRight(screen.right());
            if (rect.left() < screen.left())
                rect.moveLeft(screen.left());
            if (rect.top() < screen.top())
                rect.moveTop(qMax(mTargetRect.bottom(), screen.top()));
            if (rect.bottom() > screen.bottom())
                rect.moveBottom(screen.bottom());
        }

        setGeometry(rect);
    }

    QLabel* mLabel;
    QRect mTargetRect;
};

AutoCompleter::AutoCompleter(ScCodeEditor* editor): QObject(editor), mEditor(editor) {
    mCompletion.on = false;
    mEditor->installEventFilter(this);

    connect(editor, SIGNAL(cursorPositionChanged()), this, SLOT(onCursorChanged()));
    connect(editor->horizontalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(hideWidgets()));
    connect(editor->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(hideWidgets()));
    connect(Main::scProcess(), SIGNAL(introspectionChanged()), this, SLOT(clearMethodCallStack()));
}

void AutoCompleter::documentChanged(QTextDocument* doc) {
    connect(doc, SIGNAL(contentsChange(int, int, int)), this, SLOT(onContentsChange(int, int, int)));
}

inline QTextDocument* AutoCompleter::document() { return static_cast<QPlainTextEdit*>(mEditor)->document(); }

void AutoCompleter::keyPress(QKeyEvent* e) {
    switch (e->key()) {
    case Qt::Key_ParenLeft:
    case Qt::Key_Comma:
        triggerMethodCallAid(false);
        break;
    case Qt::Key_Backspace:
    case Qt::Key_Delete:
        return;
    default:
        qDebug(">>> key");
        // Only trigger completion if event produces at least 1 printable character:
        if (!mCompletion.on && !e->text().isEmpty() && e->text()[0].isPrint())
            triggerCompletion();
    }
}

bool AutoCompleter::eventFilter(QObject* object, QEvent* event) {
    if (object != mEditor)
        return false;

    switch (event->type()) {
    case QEvent::FocusOut:
        hideWidgets();
        break;
    case QEvent::ShortcutOverride: {
        QKeyEvent* kevent = static_cast<QKeyEvent*>(event);
        switch (kevent->key()) {
        case Qt::Key_Left:
        case Qt::Key_Right:
            if (mCompletion.menu && mCompletion.menu->isVisible())
                mCompletion.menu->reject();
            break;
        case Qt::Key_Escape:
            if (mCompletion.menu && mCompletion.menu->isVisible())
                mCompletion.menu->reject();
            else if (mMethodCall.menu && mMethodCall.menu->isVisible())
                mMethodCall.menu->reject();
            else if (mMethodCall.widget && mMethodCall.widget->isVisible()) {
                // disable method call aid for current call:
                Q_ASSERT(!mMethodCall.stack.isEmpty());
                mMethodCall.stack.top().suppressed = true;
                hideMethodCall();
            } else
                break;
            return true;
        }
        break;
    }
    case QEvent::KeyPress: {
        QKeyEvent* kevent = static_cast<QKeyEvent*>(event);
        switch (kevent->key()) {
        case Qt::Key_Tab:
        case Qt::Key_Backtab:
            if (trySwitchMethodCallArgument(kevent->key() == Qt::Key_Backtab)) {
                event->accept();
                return true;
            }
            break;
        }
        break;
    }
    default:;
    }

    return QObject::eventFilter(object, event);
}

void AutoCompleter::onContentsChange(int pos, int removed, int added) {
    qDebug() << ">>> contentsChange:" << pos << "-" << removed << "+" << added;

    int callIdx = 0;
    while (callIdx < mMethodCall.stack.count()) {
        MethodCall& call = mMethodCall.stack[callIdx];
        if (pos > call.position) {
            break;
        } else if (pos + removed > call.position) {
            qDebug("Method call: opening bracket deleted. popping.");
            mMethodCall.stack.remove(callIdx);
        } else {
            // Adjust method call positions.
            // FIXME: We are disregarding changes in context that defines the method call.
            // This is for reason of simplicity, and with the benefit that (irrelevant)
            // indentation changes don't destroy the method call stack
            qDebug("Method call: adjusting position: %i", call.position);
            call.position -= removed;
            call.position += added;
            qDebug("Method call: adjusted position: %i", call.position);
            ++callIdx;
        }
    }

    if (mCompletion.on) {
        if (pos < mCompletion.contextPos) {
            quitCompletion("context changed");
        } else if (pos <= mCompletion.pos + mCompletion.len) {
            QTextBlock block(document()->findBlock(mCompletion.pos));
            TokenIterator it(block, mCompletion.pos - block.position());
            Token::Type type = it.type();
            if (type == Token::Class || tokenMaybeName(type)) {
                mCompletion.len = it->length;
                mCompletion.text = tokenText(it);
            } else {
                mCompletion.len = 0;
                mCompletion.text.clear();
            }
            if (!mCompletion.menu.isNull())
                updateCompletionMenu(false);
        }
    }
}

void AutoCompleter::onCursorChanged() {
    int cursorPos = mEditor->textCursor().position();
    qDebug(">>> cursorChanged: %i", cursorPos);

    // completion
    if (mCompletion.on) {
        if (cursorPos < mCompletion.pos || cursorPos > mCompletion.pos + mCompletion.len) {
            quitCompletion("out of bounds");
        }
    }

    if (!mMethodCall.menu.isNull()) {
        qDebug("Method call: quitting menu");
        delete mMethodCall.menu;
    }

    updateMethodCall(cursorPos);
}

void AutoCompleter::triggerCompletion(bool forceShow) {
    if (mCompletion.on) {
        qDebug("AutoCompleter::triggerCompletion(): completion already started.");
        updateCompletionMenu(forceShow);
        return;
    }

    QTextCursor cursor(mEditor->textCursor());
    const int cursorPos = cursor.positionInBlock();
    QTextBlock block(cursor.block());
    TokenIterator it(block, cursorPos - 1);

    if (!it.isValid())
        return;

    const Token& triggeringToken = *it;

    if (triggeringToken.type == Token::Class) {
        if (triggeringToken.length < 3)
            return;
        mCompletion.type = ClassCompletion;
        mCompletion.pos = it.position();
        mCompletion.len = it->length;
        mCompletion.text = tokenText(it);
        mCompletion.contextPos = mCompletion.pos + 3;
        mCompletion.base = mCompletion.text;
        mCompletion.base.truncate(3);
    } else {
        TokenIterator objectIt, dotIt, nameIt;

        Token::Type objectTokenType = Token::Unknown;

        if (tokenMaybeName(it.type())) {
            nameIt = it;
            --it;
        }

        if (it.isValid() && it.character() == '.') {
            dotIt = it;
            --it;
        } else
            // don't trigger on method names without preceding dot (for now)
            return;

        if (dotIt.isValid()) {
            objectTokenType = it.type();
            switch (objectTokenType) {
            case Token::Class:
            case Token::Char:
            case Token::StringMark:
            case Token::Builtin:
            case Token::Symbol:
            case Token::SymbolMark:
            case Token::Float:
            case Token::RadixFloat:
            case Token::HexInt:
                objectIt = it;
                break;

            default:;
            }
        }

        if (!objectIt.isValid() && (!nameIt.isValid() || nameIt->length < 3))
            return;

        if (nameIt.isValid()) {
            mCompletion.pos = nameIt.position();
            mCompletion.len = nameIt->length;
            mCompletion.text = tokenText(nameIt);
        } else {
            mCompletion.pos = dotIt.position() + 1;
            mCompletion.len = 0;
            mCompletion.text.clear();
        }

        if (objectIt.isValid()) {
            mCompletion.contextPos = mCompletion.pos;
            mCompletion.base = tokenText(objectIt);
            mCompletion.tokenType = objectTokenType;
            mCompletion.type = ClassMethodCompletion;
        } else {
            mCompletion.contextPos = mCompletion.pos + 3;
            mCompletion.base = tokenText(nameIt);
            mCompletion.type = MethodCompletion;
        }
    }

    mCompletion.on = true;

    qDebug() << QStringLiteral("Completion: ON <%1>").arg(mCompletion.base);

    showCompletionMenu(forceShow);

    if (mCompletion.menu.isNull())
        mCompletion.on = false;
}

void AutoCompleter::quitCompletion(const QString& reason) {
    Q_ASSERT(mCompletion.on);

    qDebug() << QStringLiteral("Completion: OFF (%1)").arg(reason);

    if (mCompletion.menu) {
        mCompletion.menu->hide();
        mCompletion.menu->deleteLater();
        mCompletion.menu = 0;
    }

    mCompletion.on = false;
}

void AutoCompleter::showCompletionMenu(bool forceShow) {
    qDebug(">>> showCompletionMenu");

    using namespace ScLanguage;
    using ScLanguage::Method;

    Q_ASSERT(mCompletion.on);
    Q_ASSERT(mCompletion.menu.isNull());

    QPointer<CompletionMenu> menu;

    switch (mCompletion.type) {
    case ClassCompletion:
        menu = menuForClassCompletion(mCompletion, mEditor);
        break;

    case ClassMethodCompletion:
        menu = menuForClassMethodCompletion(mCompletion, mEditor);
        break;

    case MethodCompletion:
        menu = menuForMethodCompletion(mCompletion, mEditor);
        break;

    default:
        break;
    }


    if (menu == NULL)
        return;

    mCompletion.menu = menu;

    connect(menu, SIGNAL(finished(int)), this, SLOT(onCompletionMenuFinished(int)));

    QRect popupTargetRect = globalCursorRect(mCompletion.pos).adjusted(0, -5, 0, 5);

    menu->popup(popupTargetRect);

    updateCompletionMenu(forceShow);

    if (mCompletion.type == ClassCompletion && Main::settings()->value("IDE/editor/showAutocompleteHelp").toBool()) {
        connect(menu, SIGNAL(itemChanged(int)), this, SLOT(updateCompletionMenuInfo()));
        connect(menu, SIGNAL(infoClicked(QString)), this, SLOT(gotoHelp(QString)));
        updateCompletionMenuInfo();
    }
}

CompletionMenu* AutoCompleter::menuForClassCompletion(CompletionDescription const& completion, ScCodeEditor* editor) {
    using namespace ScLanguage;
    const Introspection& introspection = Main::scProcess()->introspection();

    const ClassMap& classes = introspection.classMap();

    QString min = completion.base;
    QString max = incrementedString(min);

    ClassMap::const_iterator matchStart, matchEnd;
    matchStart = classes.lower_bound(min);
    matchEnd = classes.lower_bound(max);
    if (matchStart == matchEnd) {
        qDebug() << "Completion: no class matches:" << completion.base;
        return NULL;
    }

    CompletionMenu* menu = new CompletionMenu(editor);

    for (ClassMap::const_iterator it = matchStart; it != matchEnd; ++it) {
        Class* klass = it->second.data();
        menu->addItem(new QStandardItem(klass->name));
    }

    menu->adapt();

    return menu;
}

CompletionMenu* AutoCompleter::menuForClassMethodCompletion(CompletionDescription const& completion,
                                                            ScCodeEditor* editor) {
    using namespace ScLanguage;

    const Class* klass = NULL;

    switch (completion.tokenType) {
    case Token::Float:
    case Token::RadixFloat:
    case Token::HexInt:
        // Only show completion if at least 1 character after dot
        if (!completion.base.contains(".") && completion.text.isEmpty())
            return NULL;
    default:;
    }

    klass = classForToken(completion.tokenType, completion.base);

    if (klass == NULL) {
        qDebug() << "Autocompletion not implemented for" << completion.base;
        return NULL;
    }

    QMap<QString, const Method*> methodMap; // for quick lookup
    QList<const Method*> methodList; // to keep order by class hierarchy
    do {
        foreach (const Method* method, klass->methods) {
            QString methodName = method->name.get();

            // Operators are also methods, but are not valid in
            // a method call syntax, so filter them out.
            Q_ASSERT(!methodName.isEmpty());
            if (!methodName[0].isLetter())
                continue;

            if (methodMap.value(methodName) != 0)
                continue;

            methodMap.insert(methodName, method);
            methodList.append(method);
        }
        klass = klass->superClass;
    } while (klass);

    CompletionMenu* menu = new CompletionMenu(editor);
    menu->setCompletionRole(CompletionMenu::CompletionRole);

    foreach (const Method* method, methodList) {
        QString methodName = method->name.get();
        QString detail(" [ %1 ]");

        QStandardItem* item = new QStandardItem();
        item->setText(methodName + detail.arg(method->ownerClass->name));
        item->setData(QVariant::fromValue(method), CompletionMenu::MethodRole);
        item->setData(methodName, CompletionMenu::CompletionRole);
        menu->addItem(item);
    }

    menu->adapt();

    return menu;
}

CompletionMenu* AutoCompleter::menuForMethodCompletion(CompletionDescription const& completion, ScCodeEditor* editor) {
    using namespace ScLanguage;
    const Introspection& introspection = Main::scProcess()->introspection();

    const MethodMap& methods = introspection.methodMap();

    QString min = completion.base;
    QString max = incrementedString(min);

    MethodMap::const_iterator matchStart, matchEnd;
    matchStart = methods.lower_bound(min);
    matchEnd = methods.lower_bound(max);
    if (matchStart == matchEnd) {
        qDebug() << "Completion: no method matches:" << completion.base;
        return NULL;
    }

    CompletionMenu* menu = new CompletionMenu(editor);
    menu->setCompletionRole(CompletionMenu::CompletionRole);

    for (MethodMap::const_iterator it = matchStart; it != matchEnd;) {
        const Method* method = it->second.data();

        std::pair<MethodMap::const_iterator, MethodMap::const_iterator> range = methods.equal_range(it->first);

        int count = std::distance(range.first, range.second);

        QStandardItem* item = new QStandardItem();

        QString methodName = method->name.get();
        QString detail(" [ %1 ]");
        if (count == 1) {
            item->setText(methodName + detail.arg(method->ownerClass->name));
            item->setData(QVariant::fromValue(method), CompletionMenu::MethodRole);
        } else
            item->setText(methodName + detail.arg(count));

        item->setData(methodName, CompletionMenu::CompletionRole);

        menu->addItem(item);

        it = range.second;
    }

    menu->adapt();

    return menu;
}

const ScLanguage::Class* AutoCompleter::classForToken(Token::Type tokenType, const QString& tokenString) {
    using namespace ScLanguage;
    const Introspection& introspection = Main::scProcess()->introspection();

    switch (tokenType) {
    case Token::Class: {
        const Class* klass = introspection.findClassOrWarn(tokenString);
        if (klass)
            klass = klass->metaClass;
        return klass;
    }

    case Token::Float:
    case Token::RadixFloat:
    case Token::HexInt:
        if (tokenString.contains(".")) // else it is an int
            return introspection.findClass("Float");
        else
            return introspection.findClass("Integer");

    case Token::Char:
        return introspection.findClass("Char");

    case Token::StringMark:
        return introspection.findClass("String");

    case Token::Symbol:
    case Token::SymbolMark:
        return introspection.findClass("Symbol");

    default:;
    }

    if (tokenString == QStringLiteral("true"))
        return introspection.findClass("True");

    if (tokenString == QStringLiteral("false"))
        return introspection.findClass("False");

    if (tokenString == QStringLiteral("nil"))
        return introspection.findClass("Nil");

    if (tokenString == QStringLiteral("thisProcess"))
        return introspection.findClass("Main");

    if (tokenString == QStringLiteral("thisFunction"))
        return introspection.findClass("Function");

    if (tokenString == QStringLiteral("thisMethod"))
        return introspection.findClass("Method");

    if (tokenString == QStringLiteral("thisFunctionDef"))
        return introspection.findClass("FunctionDef");

    if (tokenString == QStringLiteral("thisThread"))
        return introspection.findClass("Thread");

    if (tokenString == QStringLiteral("currentEnvironment"))
        return introspection.findClass("Environment");

    if (tokenString == QStringLiteral("topEnvironment"))
        return introspection.findClass("Environment");

    if (tokenString == QStringLiteral("inf"))
        return introspection.findClass("Float");

    return NULL;
}

void AutoCompleter::updateCompletionMenu(bool forceShow) {
    Q_ASSERT(mCompletion.on && !mCompletion.menu.isNull());

    CompletionMenu* menu = mCompletion.menu;

    if (!mCompletion.text.isEmpty()) {
        QString pattern = mCompletion.text;
        pattern.prepend("^");
        menu->model()->setFilterRegularExpression(pattern);
    } else
        menu->model()->setFilterRegularExpression(QString());

    if (menu->model()->hasChildren()) {
        menu->view()->setCurrentIndex(menu->model()->index(0, 0));
        if (forceShow || menu->currentText() != mCompletion.text) {
            if (!menu->isVisible())
                menu->setTargetRect(globalCursorRect(mCompletion.pos).adjusted(0, -5, 0, 5));
            // The Show event will adjust position.
            menu->show();
        } else
            menu->hide();
    } else
        menu->hide();

    if (mCompletion.type == ClassCompletion && Main::settings()->value("IDE/editor/showAutocompleteHelp").toBool())
        updateCompletionMenuInfo();
}

void AutoCompleter::onCompletionMenuFinished(int result) {
    qDebug("Completion: menu finished");

    if (!mCompletion.on)
        return;

    if (result) {
        QString text = mCompletion.menu->currentText();

        if (!text.isEmpty()) {
            quitCompletion("done");

            QTextCursor cursor(mEditor->textCursor());
            cursor.setPosition(mCompletion.pos);
            cursor.setPosition(mCompletion.pos + mCompletion.len, QTextCursor::KeepAnchor);
            cursor.insertText(text);

            return;
        }
    }

    // Do not cancel completion whenever menu hidden.
    // It could be hidden because of current filter yielding 0 results.

    // quitCompletion("cancelled");
}

void AutoCompleter::updateCompletionMenuInfo() {
    DocNode* node = parseHelpClass(findHelpClass(mCompletion.menu->currentText()));
    if (!node) {
        mCompletion.menu->addInfo(QString());
        return;
    }

    QString examples = parseClassElement(node, "EXAMPLES");
    if (!examples.isEmpty())
        examples.prepend("<h4>Examples</h4>");
    // MSVStudio 2013 does not concatenate multiple QStringliterals ("""") properly
    // see http://blog.qt.io/blog/2014/06/13/qt-weekly-13-qstringliteral/
    QString infos = QStringLiteral("<h4>%1</h4>%2%3<p><a href=\"%4\">go to help</a>")
                        .arg(parseClassElement(node, "SUMMARY"))
                        .arg(parseClassElement(node, "DESCRIPTION"))
                        .arg(examples)
                        .arg(mCompletion.menu->currentText());
    mCompletion.menu->addInfo(infos);
    doc_node_free_tree(node);
}

void AutoCompleter::triggerMethodCallAid(bool explicitTrigger) {
    using namespace ScLanguage;

    if (!mMethodCall.menu.isNull()) {
        qDebug("Method call: disambiguation menu already shown. Aborting.");
        return;
    }

    QTextCursor cursor(mEditor->textCursor());

    // Find the first bracket that defines a method call
    TokenIterator tokenIt;
    TokenIterator bracketIt = TokenIterator::leftOf(cursor.block(), cursor.positionInBlock());
    while (true) {
        bracketIt = ScCodeEditor::previousOpeningBracket(bracketIt);
        if (!bracketIt.isValid())
            return;

        if (bracketIt->character == '(') {
            tokenIt = bracketIt.previous();
            Token::Type tokenType = tokenIt.type();

            if (tokenIt.block() == bracketIt.block() && (tokenType == Token::Name || tokenType == Token::Class))
                break;
        }

        if (!explicitTrigger)
            return;

        --bracketIt;
    }

    int bracketPos = bracketIt.position();

    // Compare against stack
    if (!mMethodCall.stack.isEmpty() && mMethodCall.stack.top().position == bracketPos) {
        // A matching call is already on stack
        qDebug("Method call: trigger -> call already on stack");

        // If triggered explicitly, then either retrigger disambiguation (if needed),
        // or unsuppress it.
        if (explicitTrigger && !mMethodCall.stack.top().method) {
            qDebug("Method call: forced re-trigger, popping current call.");
            mMethodCall.stack.pop();
            hideMethodCall();
        } else {
            if (explicitTrigger) {
                mMethodCall.stack.top().suppressed = false;
                updateMethodCall(cursor.position());
            }
            // Else, method call popup has been updated by updateMethodCall()
            // called on cursor change, before this function.
            return;
        }
    }

    QString methodName;
    bool functionalNotation = false;
    const Class* receiverClass = NULL;
    Token::Type tokenType = tokenIt.type();

    Q_ASSERT(tokenType == Token::Name || tokenType == Token::Class);

    if (tokenType == Token::Name) {
        methodName = tokenText(tokenIt);
        --tokenIt;
        if (tokenIt.isValid() && tokenIt.character() == '.')
            --tokenIt;
        else
            functionalNotation = true;
    } else
        methodName = "new";

    if (!functionalNotation && tokenIt.isValid())
        receiverClass = classForToken(tokenIt->type, tokenText(tokenIt));

    // Ok, this is a valid method call, push on stack

    qDebug("Method call: found call: %s:%s", receiverClass ? qPrintable(receiverClass->name.get()) : "",
           methodName.toStdString().c_str());

    qDebug("Method call: new call");
    MethodCall call;
    call.position = bracketPos;
    call.functionalNotation = functionalNotation;
    pushMethodCall(call);

    // Obtain method data, either by inferrence or by user-disambiguation via a menu

    const Method* method = 0;

    if (receiverClass) {
        const Class* klass = receiverClass;
        do {
            foreach (const Method* m, klass->methods) {
                if (m->name == methodName) {
                    method = m;
                    break;
                }
            }
            if (method)
                break;
            klass = klass->superClass;
        } while (klass);
    } else {
        method = disambiguateMethod(methodName, bracketPos);
    }

    // Finally, show the aid for the method

    if (method) {
        Q_ASSERT(!mMethodCall.stack.isEmpty());
        mMethodCall.stack.top().method = method;
        updateMethodCall(mEditor->textCursor().position());
    }
}

const ScLanguage::Method* AutoCompleter::disambiguateMethod(const QString& methodName, int cursorPos) {
    Q_ASSERT(mMethodCall.menu.isNull());

    using namespace ScLanguage;
    using std::pair;

    const Introspection& introspection = Main::scProcess()->introspection();
    const MethodMap& methods = introspection.methodMap();

    pair<MethodMap::const_iterator, MethodMap::const_iterator> match = methods.equal_range(methodName);

    const Method* method = 0;

    if (match.first == match.second) {
        qDebug() << "MethodCall: no method matches:" << methodName;
        method = 0;
    } else if (std::distance(match.first, match.second) == 1)
        method = match.first->second.data();
    else {
        QPointer<CompletionMenu> menu = new CompletionMenu(mEditor);
        mMethodCall.menu = menu;

        for (MethodMap::const_iterator it = match.first; it != match.second; ++it) {
            const Method* method = it->second.data();
            QStandardItem* item = new QStandardItem();
            item->setText(method->name + " (" + method->ownerClass->name + ')');
            item->setData(QVariant::fromValue(method), CompletionMenu::MethodRole);
            menu->addItem(item);
        }

        menu->adapt();

        QRect popupTargetRect = globalCursorRect(cursorPos).adjusted(0, -5, 0, 5);

        if (static_cast<PopUpWidget*>(menu)->exec(popupTargetRect))
            method = menu->currentMethod();

        delete menu;
    }

    return method;
}

void AutoCompleter::updateMethodCall(int cursorPos) {
    int i = mMethodCall.stack.count();
    while (i--) {
        MethodCall& call = mMethodCall.stack[i];
        int argNum = -1;
        TokenIterator argNameToken;

        if (!testMethodCall(call, cursorPos, argNum, argNameToken)) {
            qDebug("Method call: popping.");
            Q_ASSERT(i == mMethodCall.stack.count() - 1);
            mMethodCall.stack.pop();
            continue;
        }

        if (call.suppressed) {
            qDebug("Method call: suppressed, not showing anything");
            break;
        }

        if (!call.method || !call.method->arguments.count()) {
            qDebug("Method call: no info to show. skipping.");
            continue;
        }

        if (argNameToken.isValid()) {
            QString argName = tokenText(argNameToken);
            argName.chop(1);
            for (int idx = 0; idx < call.method->arguments.count(); ++idx) {
                if (call.method->arguments[idx].name == argName) {
                    argNum = idx;
                    if (call.functionalNotation)
                        ++argNum;
                    break;
                }
            }
        }
        qDebug("Method call: found current call: %s(%i)", call.method->name.get().toStdString().c_str(), argNum);
        showMethodCall(call, argNum);
        return;
    }

    hideMethodCall();
}

void AutoCompleter::pushMethodCall(const MethodCall& call) {
    qDebug("Method Call: pushing on stack.");
    Q_ASSERT(mMethodCall.stack.isEmpty() || mMethodCall.stack.last().position < call.position);

    mMethodCall.stack.push(call);
}

void AutoCompleter::showMethodCall(const MethodCall& call, int arg) {
    if (mMethodCall.widget.isNull())
        mMethodCall.widget = new MethodCallWidget(mEditor);

    MethodCallWidget* w = mMethodCall.widget;

    w->showMethod(call, arg, globalCursorRect(call.position).adjusted(0, -7, 0, 5));
}

void AutoCompleter::hideMethodCall() { delete mMethodCall.widget; }

bool AutoCompleter::trySwitchMethodCallArgument(bool backwards) {
    // FIXME: Only cycle through argument names that have not been entered already

    using namespace ScLanguage;

    QTextCursor cursor(mEditor->textCursor());
    if (cursor.hasSelection())
        return false;

    if (mMethodCall.stack.isEmpty()) {
        qDebug("Insert arg name: empty stack");
        return false;
    }

    MethodCall& call = mMethodCall.stack.top();
    if (!call.method || !call.method->arguments.count()) {
        qDebug("Insert arg name: no method, or method has no args");
        return false;
    }

    int cursorPos = cursor.position();
    int argNum = -1;
    TokenIterator argNameToken;

    static const bool strict = true;
    bool callValid = testMethodCall(call, cursorPos, argNum, argNameToken, strict);
    if (!callValid) {
        qDebug("Insert arg name: call invalid");
        return false;
    }

    bool cursorAtArgName = argNum == -1;

    if (argNameToken.isValid()) {
        qDebug("Insert arg name: have a reference arg name");
        QString argName = tokenText(argNameToken);
        argName.chop(1);
        for (int idx = 0; idx < call.method->arguments.count(); ++idx) {
            if (call.method->arguments[idx].name == argName) {
                argNum = idx;
                break;
            }
        }
        // only increment/decrement if a reference name exists
        if (backwards)
            --argNum;
        else
            ++argNum;
    }

    // limit / wrap
    if (argNum < 0)
        argNum = call.method->arguments.count() - 1;
    else if (argNum >= call.method->arguments.count())
        argNum = 0;

    QString text = call.method->arguments[argNum].name;
    text.append(": ");

    // insert argument name
    if (argNameToken.isValid() && cursorAtArgName) {
        int pos = argNameToken.position();
        cursor.setPosition(pos);
        cursor.setPosition(pos + argNameToken->length + 1, QTextCursor::KeepAnchor);
    }
    cursor.insertText(text);

    return true;
}

bool AutoCompleter::testMethodCall(const MethodCall& call, int cursorPos, int& outArgNum,
                                   TokenIterator& outArgNameToken, bool strict) {
    // The 'strict' argument denotes whether the test passes if token before cursor is
    // not a comma or an argument name - i.e. the user is typing the value of an argument

    if (call.position >= cursorPos) {
        qDebug("Method call: call right of cursor.");
        return false;
    }

    QTextBlock block(document()->findBlock(call.position));
    TokenIterator token = TokenIterator::rightOf(block, call.position - block.position());
    if (!token.isValid())
        qWarning("Method call: call stack out of sync!");
    Q_ASSERT(token.isValid());

    int argNum = 0;
    int level = 1;
    TokenIterator argNameToken;
    bool strictlyValid = true;

    ++token;
    while (level > 0 && token.isValid() && token.position() < cursorPos) {
        strictlyValid = false;

        char chr = token.character();
        Token::Type type = token->type;
        if (level == 1) {
            if (type == Token::SymbolArg) {
                argNameToken = token;
                argNum = -1; // denote argument name
                strictlyValid = true;
            } else if (chr == ',') {
                if (argNum >= 0)
                    ++argNum;
                else
                    argNum = -2; // denote comma after argument name
                strictlyValid = true;
            }
        }

        if (type == Token::OpeningBracket)
            ++level;
        else if (type == Token::ClosingBracket)
            --level;

        ++token;
    }

    if (level <= 0) {
        qDebug("Method call: call left of cursor.");
        return false;
    }

    outArgNameToken = argNameToken;
    outArgNum = argNum;

    return (strictlyValid || !strict);
}

void AutoCompleter::clearMethodCallStack() {
    mMethodCall.stack.clear();
    hideMethodCall();
}

void AutoCompleter::hideWidgets() {
    if (mCompletion.menu)
        mCompletion.menu->reject();
    if (mMethodCall.menu)
        mMethodCall.menu->reject();
    if (mMethodCall.widget)
        mMethodCall.widget->hide();
}

QString AutoCompleter::tokenText(TokenIterator& it) {
    if (!it.isValid())
        return QString();

    int pos = it.position();
    QTextCursor cursor(document());
    cursor.setPosition(pos);
    cursor.setPosition(pos + it->length, QTextCursor::KeepAnchor);
    return cursor.selectedText();
}

QRect AutoCompleter::globalCursorRect(int cursorPosition) {
    QTextCursor cursor(document());
    cursor.setPosition(cursorPosition);
    QRect r = mEditor->cursorRect(cursor);
    r.moveTopLeft(mEditor->viewport()->mapToGlobal(r.topLeft()));
    return r;
}

QString AutoCompleter::findHelpClass(QString klass) {
    QString file = standardDirectory(ScResourceDir).append("/HelpSource/Classes/").append(klass).append(".schelp");
    if (QFile::exists(file))
        return file;

    return QString();
}

DocNode* AutoCompleter::parseHelpClass(QString file) {
    if (file.isEmpty())
        return NULL;

    // note, toStdString() converts to UTF-8
    return scdoc_parse_file(file.toStdString(), 0);
}

QString AutoCompleter::parseClassElement(DocNode* node, QString element) {
    if (QString(node->id) == element) {
        QString str;
        parseClassNode(node, &str);
        return str;
    }

    for (int i = 0; i < node->n_childs; i++) {
        QString ret = parseClassElement(node->children[i], element);
        if (!ret.isEmpty())
            return ret;
    }

    return QString();
}

void AutoCompleter::parseClassNode(DocNode* node, QString* str) {
    QString id = node->id;

    if (id == "NOTE")
        str->append("<br><br>Note:<br>");

    if (node->text) {
        if (id == "LINK") {
            QStringList locations = QString(node->text).split('/').last().split('#');

            /* if empty, the link is on the same page. No HTML link */
            if (locations.first().isEmpty())
                str->append(QStringLiteral(" %1 ").arg(locations.first()));
            else
                str->append(QStringLiteral("<a href=\"%1\">%2</a>").arg(locations.first()).arg(locations.last()));
        } else if (id == "CODE") {
            str->append(QStringLiteral("<code>%1</code>").arg(node->text));
        } else if (id == "CODEBLOCK") {
            str->append(QStringLiteral("<pre><code>%1</code></pre>").arg(node->text));
        } else {
            str->append(node->text);
        }
    }

    for (int i = 0; i < node->n_childs; i++)
        parseClassNode(node->children[i], str);
}

void AutoCompleter::gotoHelp(QString symbol) {
#ifdef SC_USE_QTWEBENGINE
    HelpBrowserDocklet* helpDock = MainWindow::instance()->helpBrowserDocklet();
    helpDock->browser()->gotoHelpFor(symbol);
    helpDock->focus();
#endif // SC_USE_QTWEBENGINE
}

} // namespace ScIDE

#undef QT_NO_DEBUG_OUTPUT
