#include "AsmView.h"
#include <AsmViewModel.h>

#include <QApplication>
#include <QClipboard>
#include <QContextMenuEvent>
#include <QMenu>
#include <QPainter>
#include <QStyledItemDelegate>

#include <KLocalizedString>
#include <KSyntaxHighlighting/Theme>
#include <KTextEditor/Editor>

#include <drawing_utils.h>
#include <ktexteditor_utils.h>

class LineNumberDelegate : public QStyledItemDelegate
{
public:
    explicit LineNumberDelegate(QObject *parent)
        : QStyledItemDelegate(parent)
    {
        auto updateColors = [this] {
            auto e = KTextEditor::Editor::instance();
            auto theme = e->theme();
            lineNumColor = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::LineNumbers));
            borderColor = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::Separator));
            currentLineColor = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::CurrentLineNumber));
            iconBorderColor = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::IconBorder));
        };
        updateColors();
        connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateColors);
    }

    void paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const override
    {
        QStyleOptionViewItem option = opt;
        initStyleOption(&option, index);

        painter->save();

        QString text = option.text;
        option.text = QString();

        auto iconBorder = option.rect;

        QColor textColor = lineNumColor;
        // paint background
        if (option.state & QStyle::State_Selected) {
            textColor = currentLineColor;
            painter->fillRect(option.rect, option.palette.highlight());
        } else {
            painter->fillRect(iconBorder, iconBorderColor);
        }

        option.widget->style()->drawControl(QStyle::CE_ItemViewItem, &option, painter, option.widget);

        iconBorder.setRight(iconBorder.right() - 1); // leave space for separator

        painter->setPen(borderColor);
        painter->drawLine(iconBorder.topRight(), iconBorder.bottomRight());

        auto textRect = option.widget->style()->subElementRect(QStyle::SE_ItemViewItemText, &option, option.widget);
        textRect.setRight(textRect.right() - 5); // 4 px padding
        painter->setFont(index.data(Qt::FontRole).value<QFont>());
        painter->setPen(textColor);
        painter->drawText(textRect, Qt::AlignRight | Qt::AlignVCenter, text);

        painter->restore();
    }

private:
    QColor currentLineColor;
    QColor borderColor;
    QColor lineNumColor;
    QColor iconBorderColor;
};

class CodeDelegate : public QStyledItemDelegate
{
public:
    explicit CodeDelegate(QObject *parent)
        : QStyledItemDelegate(parent)
    {
        auto updateColors = [this] {
            auto e = KTextEditor::Editor::instance();
            auto theme = e->theme();

            normalColor = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Normal));
            keywordColor = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Keyword));
            funcColor = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Function));
            stringColor = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::String));
        };
        updateColors();
        connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateColors);
    }

    void paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const override
    {
        QStyleOptionViewItem option = opt;
        initStyleOption(&option, index);

        painter->save();

        QString text = option.text;
        option.text = QString();

        option.widget->style()->drawControl(QStyle::CE_ItemViewItem, &option, painter, option.widget);

        const auto *m = static_cast<const AsmViewModel *>(index.model());
        if (m->hasError()) {
            drawTextWithErrors(painter, option, text);
            painter->restore();
            return;
        }

        QList<QTextLayout::FormatRange> fmts;

        // is a label?
        if (!text.isEmpty() && !text.at(0).isSpace()) {
            QTextCharFormat f;
            f.setForeground(funcColor);
            int colon = findColon(text);
            if (colon > -1) {
                fmts.append({.start = 0, .length = colon + 1, .format = f});
            }

        } else {
            int i = firstNotSpace(text);
            int nextSpace = text.indexOf(u' ', i + 1);

            // If there is no space then this is the only word on the line
            // e.g "ret"
            if (nextSpace == -1) {
                nextSpace = text.length();
            }

            QTextCharFormat f;

            if (i >= 0 && nextSpace > i) {
                f.setForeground(keywordColor);
                fmts.append({.start = i, .length = nextSpace - i, .format = f});

                i = nextSpace + 1;
            }

            const auto [strOpen, strClose] = getStringPos(text, i);
            if (strOpen >= 0) {
                f = QTextCharFormat();
                f.setForeground(stringColor);
                fmts.append({.start = strOpen, .length = strClose - strOpen, .format = f});
            }

            const auto labels = rowLabels(index);
            if (!labels.isEmpty()) {
                f = QTextCharFormat();
                f.setForeground(funcColor);
                f.setUnderlineStyle(QTextCharFormat::SingleUnderline);
                for (const auto &label : labels) {
                    fmts.append({.start = label.col, .length = label.len, .format = f});
                }
            }
        }

        Utils::paintItemViewText(painter, text, option, fmts);

        painter->restore();
    }

    static int firstNotSpace(const QString &text)
    {
        for (int i = 0; i < text.size(); ++i) {
            if (!text.at(i).isSpace()) {
                return i;
            }
        }
        return 0;
    }

    struct StringRange {
        int open{};
        int close{};
    };
    static StringRange getStringPos(const QString &text, int from)
    {
        int open = text.indexOf(u'"', from);
        if (open == -1) {
            return {-1, -1};
        }
        int close = text.indexOf(u'"', open + 1);
        if (close == -1) {
            return {-1, -1};
        }
        return {open, close + 1}; // +1 because we include the quote as well
    }

    static int findColon(const QString &text, int from = 0)
    {
        int colon = text.indexOf(u':', from);
        if (colon == -1) {
            return -1;
        }

        if (colon + 1 >= text.length()) {
            return colon;
        }

        if (text.at(colon + 1) != u':') {
            return colon;
        }

        colon += 2;

        auto isLabelEnd = [text](int &i) {
            if (text.at(i) == u':') {
                // reached end, good enough to be a label
                if (i + 1 >= text.length()) {
                    return true;
                }
                if (text.at(i + 1) != u':') {
                    return true;
                }
                i++;
            }
            return false;
        };

        for (int i = colon; i < text.length(); i++) {
            if (isLabelEnd(i)) {
                return i;
            }
        }

        return -1;
    }

    static QList<LabelInRow> rowLabels(const QModelIndex &index)
    {
        return index.data(AsmViewModel::RowLabels).value<QList<LabelInRow>>();
    }

    void drawTextWithErrors(QPainter *p, const QStyleOptionViewItem &option, const QString &text) const
    {
        QList<QTextLayout::FormatRange> fmts;

        int errIdx = text.indexOf(QLatin1String("error:"));
        if (errIdx != -1) {
            QTextCharFormat f;
            f.setForeground(keywordColor);
            fmts.append({.start = errIdx, .length = 5, .format = f});
        }

        Utils::paintItemViewText(p, text, option, fmts);
    }

private:
    QColor keywordColor;
    QColor funcColor;
    QColor normalColor;
    QColor stringColor;
};

AsmView::AsmView(QWidget *parent)
    : QTreeView(parent)
{
    setUniformRowHeights(true);
    setRootIsDecorated(false);
    setHeaderHidden(true);
    setSelectionMode(QAbstractItemView::ContiguousSelection);

    setItemDelegateForColumn(0, new LineNumberDelegate(this));
    setItemDelegateForColumn(1, new CodeDelegate(this));

    auto updateColors = [this] {
        auto e = KTextEditor::Editor::instance();
        auto theme = e->theme();
        auto palette = this->palette();
        QColor c = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::CurrentLine));
        palette.setColor(QPalette::Highlight, c);
        c = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Normal));
        palette.setColor(QPalette::Text, c);
        c = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor));
        palette.setColor(QPalette::Base, c);
        setPalette(palette);

        auto model = static_cast<AsmViewModel *>(this->model());
        if (!model) {
            qWarning("%s Unexpected null model!", Q_FUNC_INFO);
            return;
        }
        model->setFont(Utils::editorFont());
    };
    QMetaObject::invokeMethod(this, updateColors, Qt::QueuedConnection);
    connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateColors);
}

void AsmView::contextMenuEvent(QContextMenuEvent *e)
{
    QPoint p = e->pos();

    QMenu menu(this);
    menu.addAction(i18n("Scroll to source"), this, [this, p] {
        auto model = static_cast<AsmViewModel *>(this->model());
        int line = model->sourceLineForAsmLine(indexAt(p));
        Q_EMIT scrollToLineRequested(line);
    });

    QModelIndex index = indexAt(e->pos());
    if (index.isValid()) {
        const auto labels = index.data(AsmViewModel::RowLabels).value<QList<LabelInRow>>();
        if (!labels.isEmpty()) {
            menu.addAction(i18n("Jump to label"), this, [this, index, labels] {
                auto model = static_cast<AsmViewModel *>(this->model());

                const QString asmLine = index.data().toString();

                auto labelInRow = labels.first();
                QString label = asmLine.mid(labelInRow.col, labelInRow.len);
                int line = model->asmLineForLabel(label);

                if (line != -1) {
                    auto labelIdx = model->index(line - 1, 1);
                    scrollTo(labelIdx, ScrollHint::PositionAtCenter);
                    if (selectionModel()) {
                        selectionModel()->select(labelIdx, QItemSelectionModel::ClearAndSelect);
                    }
                }
            });
        }
    }

    if (!selectedIndexes().isEmpty()) {
        menu.addAction(i18n("Copy"), this, [this] {
            const auto selected = selectedIndexes();
            QString text;
            for (const auto idx : selected) {
                if (idx.column() == AsmViewModel::Column_LineNo)
                    continue;
                text += idx.data().toString() + QStringLiteral("\n");
            }
            qApp->clipboard()->setText(text);
        });
    }

    menu.addAction(i18n("Select All"), this, [this] {
        if (auto sm = selectionModel()) {
            QItemSelection sel;
            auto start = model()->index(0, 0);
            auto end = model()->index(model()->rowCount() - 1, model()->columnCount() - 1);
            sel.select(start, end);
            sm->select(sel, QItemSelectionModel::ClearAndSelect);
        }
    });

    menu.exec(mapToGlobal(p));
}

#include "moc_AsmView.cpp"
