/*
    SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "kateurlbar.h"
#include "kateapp.h"
#include "katemainwindow.h"
#include "kateviewmanager.h"
#include "ktexteditor_utils.h"

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

#include <KColorScheme>
#include <KLocalizedString>

#include <QAbstractListModel>
#include <QApplication>
#include <QClipboard>
#include <QDir>
#include <QHBoxLayout>
#include <QIcon>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QListView>
#include <QMenu>
#include <QMimeDatabase>
#include <QPainter>
#include <QScrollBar>
#include <QSortFilterProxyModel>
#include <QStackedWidget>
#include <QStandardItemModel>
#include <QStyledItemDelegate>
#include <QTimer>
#include <QToolButton>
#include <QTreeView>
#include <QUrl>

#include <KFuzzyMatcher>

using namespace std::chrono_literals;

class FuzzyFilterModel final : public QSortFilterProxyModel
{
public:
    explicit FuzzyFilterModel(QObject *parent = nullptr)
        : QSortFilterProxyModel(parent)
    {
        connect(this, &FuzzyFilterModel::modelAboutToBeReset, this, [this] {
            m_pattern.clear();
        });
    }

    bool filterAcceptsRow(int row, const QModelIndex &parent) const override
    {
        if (m_pattern.isEmpty()) {
            return true;
        }

        const QModelIndex index = sourceModel()->index(row, filterKeyColumn(), parent);
        const QString text = index.data(filterRole()).toString();
        const bool res = KFuzzyMatcher::matchSimple(m_pattern, text);
        return res;
    }

    void setFilterString(const QString &text)
    {
        beginResetModel();
        m_pattern = text;
        endResetModel();
    }

private:
    QString m_pattern;
};

class BaseFilterItemView : public QWidget
{
    Q_OBJECT
public:
    explicit BaseFilterItemView(QWidget *parent = nullptr)
        : QWidget(parent)
    {
    }

Q_SIGNALS:
    void returnPressed(const QModelIndex &, Qt::KeyboardModifiers);
    void clicked(const QModelIndex &, Qt::KeyboardModifiers);
};

template<class ItemView>
class FilterableItemView : public BaseFilterItemView
{
public:
    explicit FilterableItemView(QWidget *parent = nullptr)
        : BaseFilterItemView(parent)
    {
        auto layout = new QVBoxLayout(this);
        layout->setContentsMargins({});
        layout->setSpacing(0);
        layout->addWidget(&m_filterText);
        layout->addWidget(&m_itemView);

        m_filterText.setReadOnly(true);
        m_filterText.hide();

        m_itemView.installEventFilter(this);
        m_itemView.viewport()->installEventFilter(this);
        setFocusProxy(&m_itemView);

        m_proxyModel.setFilterKeyColumn(0);
        m_proxyModel.setFilterRole(Qt::DisplayRole);
        if constexpr (std::is_same_v<ItemView, QTreeView>) {
            m_proxyModel.setRecursiveFilteringEnabled(true);
        }
        m_itemView.setModel(&m_proxyModel);
        connect(&m_filterText, &QLineEdit::textChanged, &m_proxyModel, &FuzzyFilterModel::setFilterString);
        connect(&m_filterText, &QLineEdit::textChanged, this, [this] {
            auto index = m_proxyModel.index(0, 0);
            if (index.isValid()) {
                m_itemView.setCurrentIndex(index);
                if constexpr (std::is_same_v<ItemView, QTreeView>) {
                    m_itemView.expandAll();
                }
            }
        });
    }

    void setModel(QAbstractItemModel *model)
    {
        m_proxyModel.setSourceModel(model);
    }

    QAbstractItemModel *model()
    {
        return m_itemView.model();
    }

    ItemView *view()
    {
        return &m_itemView;
    }

    void setCurrentIndex(const QModelIndex &index)
    {
        m_itemView.setCurrentIndex(m_proxyModel.mapFromSource(index));
    }
    QModelIndex currentIndex() const
    {
        return m_itemView.currentIndex();
    }

protected:
    bool eventFilter(QObject *o, QEvent *e) override
    {
        if (e->type() == QEvent::MouseButtonPress) {
            auto *me = static_cast<QMouseEvent *>(e);
            if (me->button() == Qt::LeftButton) {
                const QModelIndex idx = m_itemView.indexAt(m_itemView.viewport()->mapFromGlobal(me->globalPosition().toPoint()));
                if (!idx.isValid()) {
                    return QWidget::eventFilter(o, me);
                }
                m_filterText.hide();
                Q_EMIT clicked(idx, me->modifiers());
            }
        }

        if (e->type() == QEvent::KeyPress) {
            auto *keyEvent = static_cast<QKeyEvent *>(e);
            if ((keyEvent->modifiers() == Qt::NoModifier || keyEvent->modifiers() == Qt::SHIFT) && !keyEvent->text().isEmpty()) {
                QChar c = keyEvent->text().front();
                if (c.isPrint()) {
                    m_filterText.setText(m_filterText.text() + keyEvent->text());
                    if (!m_filterText.isVisible()) {
                        m_filterText.show();
                    }
                    return true;
                } else if (keyEvent->key() == Qt::Key_Backspace) {
                    if (m_filterText.text().isEmpty()) {
                        return QWidget::eventFilter(o, e);
                    }
                    m_filterText.setText(m_filterText.text().chopped(1));
                    if (m_filterText.text().isEmpty()) {
                        m_filterText.hide();
                    }
                    return true;
                } else if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) {
                    m_filterText.blockSignals(true);
                    m_filterText.clear();
                    m_filterText.blockSignals(false);
                    m_filterText.hide();
                    Q_EMIT returnPressed(m_itemView.currentIndex(), keyEvent->modifiers());
                }
            }
        }

        return QWidget::eventFilter(o, e);
    }

private:
    ItemView m_itemView;
    QLineEdit m_filterText;
    FuzzyFilterModel m_proxyModel;
};
using FilterableListView = FilterableItemView<QListView>;
using FilterableTreeView = FilterableItemView<QTreeView>;

class DirFilesModel : public QAbstractListModel
{
public:
    DirFilesModel(QObject *parent = nullptr)
        : QAbstractListModel(parent)
    {
    }

    enum Role {
        FileInfo = Qt::UserRole + 1
    };

    int rowCount(const QModelIndex & = {}) const override
    {
        return m_fileInfos.size();
    }

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
    {
        if (!index.isValid()) {
            return {};
        }

        const QFileInfo &fi = m_fileInfos.at(index.row());
        if (role == Qt::DisplayRole) {
            return fi.fileName();
        } else if (role == Qt::DecorationRole) {
            if (!m_decorationsEnabled) {
                return {};
            }
            if (fi.isDir()) {
                return QIcon(QIcon::fromTheme(QStringLiteral("folder")));
            } else if (fi.isFile()) {
                return QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(fi, QMimeDatabase::MatchExtension).iconName());
            }
        } else if (role == FileInfo) {
            return QVariant::fromValue(fi);
        } else if (role == Qt::ForegroundRole) {
            // highlight already open documents
            if (KateApp::self()->documentManager()->findDocument(QUrl::fromLocalFile(fi.absoluteFilePath()))) {
                return KColorScheme().foreground(KColorScheme::PositiveText).color();
            }
        }

        return {};
    }

    void setDir(const QDir &dir)
    {
        m_fileInfos.clear();
        m_currentDir = dir;

        beginResetModel();
        m_fileInfos = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::Hidden);
        endResetModel();
    }

    QDir dir() const
    {
        return m_currentDir;
    }

    void enableDecorationRole()
    {
        m_decorationsEnabled = true;
    }

private:
    bool m_decorationsEnabled = false;
    QList<QFileInfo> m_fileInfos;
    QDir m_currentDir;
};

class DirFilesList : public QMenu
{
    Q_OBJECT
public:
    DirFilesList(QWidget *parent)
        : QMenu(parent)
    {
        m_list.setModel(&m_model);
        m_list.view()->setResizeMode(QListView::Adjust);
        m_list.view()->setViewMode(QListView::ListMode);
        m_list.view()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        m_list.view()->setFrameStyle(QFrame::NoFrame);

        auto *l = new QVBoxLayout(this);
        l->setContentsMargins({});
        l->addWidget(&m_list);

        m_list.view()->viewport()->installEventFilter(this);
        setFocusProxy(&m_list);

        connect(&m_list, &FilterableListView::returnPressed, this, &DirFilesList::onClicked);
        connect(&m_list, &FilterableListView::clicked, this, &DirFilesList::onClicked);
        connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, &DirFilesList::updatePalette, Qt::QueuedConnection);
    }

    void updatePalette()
    {
        auto p = m_list.palette();
        p.setBrush(QPalette::Base, palette().alternateBase());
        m_list.setPalette(p);
    }

    void setDir(const QDir &d, const QString &currentItemName)
    {
        m_model.setDir(d);
        updateGeometry();
        auto firstIndex = m_model.index(0, 0);
        if (!currentItemName.isEmpty() && firstIndex.isValid()) {
            const auto idxesToSelect = m_model.match(firstIndex, Qt::DisplayRole, currentItemName);
            if (!idxesToSelect.isEmpty() && idxesToSelect.constFirst().isValid()) {
                m_list.setCurrentIndex(idxesToSelect.constFirst());
            }
        } else {
            m_list.setCurrentIndex(firstIndex);
        }
    }

    void updateGeometry()
    {
        auto rowHeight = m_list.view()->sizeHintForRow(0);
        auto c = m_model.rowCount();
        const auto h = (rowHeight * c) + 4;
        const auto vScroll = m_list.view()->verticalScrollBar();
        int w = m_list.view()->sizeHintForColumn(0) + (vScroll ? vScroll->height() / 2 : 0);

        setFixedSize(qMin(w, 500), qMin(h, 600));

        // enable decorations after resizing is finished
        m_model.enableDecorationRole();
    }

    void onClicked(const QModelIndex &idx, Qt::KeyboardModifiers mod)
    {
        if (!idx.isValid()) {
            return;
        }
        const auto fi = idx.data(DirFilesModel::FileInfo).value<QFileInfo>();
        if (fi.isDir()) {
            setDir(QDir(fi.absoluteFilePath()), QString());
        } else if (fi.isFile()) {
            const QUrl url = QUrl::fromLocalFile(fi.absoluteFilePath());
            hide();
            Q_EMIT openUrl(url, /*newtab=*/mod);
        }
    }

    void keyPressEvent(QKeyEvent *ke) override
    {
        if (ke->key() == Qt::Key_Left || ke->key() == Qt::Key_Right) {
            hide();
            Q_EMIT navigateLeftRight(ke->key());
        } else if (ke->key() == Qt::Key_Escape) {
            hide();
            ke->accept();
            return;
        } else if (ke->key() == Qt::Key_Backspace) {
            QDir dir = m_model.dir();
            if (dir.cdUp()) {
                setDir(dir, QString());
            }
        }
        QMenu::keyPressEvent(ke);
    }

Q_SIGNALS:
    void openUrl(const QUrl &url, Qt::KeyboardModifiers);
    void navigateLeftRight(int key);

private:
    FilterableListView m_list;
    DirFilesModel m_model;
};

class SymbolsTreeView : public QMenu
{
    Q_OBJECT
public:
    // Copied from LSPClientSymbolView
    enum Role {
        SymbolRange = Qt::UserRole,
        ScoreRole, //> Unused here
        IsPlaceholder
    };
    SymbolsTreeView(QWidget *parent)
        : QMenu(parent)
    {
        m_tree.view()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        m_tree.view()->setFrameStyle(QFrame::NoFrame);
        m_tree.view()->setUniformRowHeights(true);
        m_tree.view()->setHeaderHidden(true);
        m_tree.view()->setTextElideMode(Qt::ElideRight);
        m_tree.view()->setRootIsDecorated(false);

        auto *l = new QVBoxLayout(this);
        l->setContentsMargins({});
        l->addWidget(&m_tree);
        setFocusProxy(&m_tree);
        m_tree.view()->installEventFilter(this);
        m_tree.view()->viewport()->installEventFilter(this);

        connect(&m_tree, &FilterableTreeView::clicked, this, &SymbolsTreeView::onClicked);
        connect(&m_tree, &FilterableTreeView::returnPressed, this, &SymbolsTreeView::onClicked);
        connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, &SymbolsTreeView::updatePalette, Qt::QueuedConnection);
    }

    void updatePalette()
    {
        auto p = m_tree.palette();
        p.setBrush(QPalette::Base, palette().alternateBase());
        m_tree.setPalette(p);
    }

    void setSymbolsModel(QAbstractItemModel *model, KTextEditor::View *v, const QString &text)
    {
        m_activeView = v;
        m_tree.setModel(model);
        m_tree.view()->expandAll();
        const auto idxToSelect = m_tree.model()->match(model->index(0, 0), 0, text, 1, Qt::MatchExactly | Qt::MatchRecursive);

        updateGeometry();

        if (!idxToSelect.isEmpty()) {
            m_tree.view()->selectionModel()->setCurrentIndex(idxToSelect.constFirst(), QItemSelectionModel::ClearAndSelect);
            m_tree.view()->scrollTo(idxToSelect.constFirst());
        }
    }

    bool eventFilter(QObject *o, QEvent *e) override
    {
        // Handling via event filter is necessary to bypass
        // QTreeView's own key event handling
        if (e->type() == QEvent::KeyPress) {
            if (handleKeyPressEvent(static_cast<QKeyEvent *>(e))) {
                return true;
            }
        }

        return QMenu::eventFilter(o, e);
    }

    bool handleKeyPressEvent(QKeyEvent *ke)
    {
        if (ke->key() == Qt::Key_Left) {
            hide();
            Q_EMIT navigateLeftRight(ke->key());
            return false;
        } else if (ke->key() == Qt::Key_Escape) {
            hide();
            ke->accept();
            return true;
        }
        return false;
    }

    void updateGeometry()
    {
        const auto *model = m_tree.model();
        const int rows = rowCount(model, {});
        const int rowHeight = m_tree.view()->sizeHintForRow(0);
        const int maxHeight = rows * rowHeight;

        setFixedSize(350, qMin(600, maxHeight));
    }

    void onClicked(const QModelIndex &idx)
    {
        const auto range = idx.data(SymbolRange).value<KTextEditor::Range>();
        if (range.isValid()) {
            m_activeView->setCursorPosition(range.start());
        }
        hide();
    }

    static QModelIndex symbolForCurrentLine(QAbstractItemModel *model, const QModelIndex &index, int line)
    {
        const int rowCount = model->rowCount(index);
        for (int i = 0; i < rowCount; ++i) {
            const auto idx = model->index(i, 0, index);
            if (idx.data(SymbolRange).value<KTextEditor::Range>().overlapsLine(line)) {
                if (model->hasChildren(idx)) {
                    const auto childIdx = symbolForCurrentLine(model, idx, line);
                    if (childIdx.isValid()) {
                        return childIdx;
                    }
                }
                return idx;
            }
        }
        return {};
    }

private:
    // row count that counts top level + 1 level down rows
    // needed to ensure we don't get strange heights for
    // cases where there are only a couple of top level symbols
    int rowCount(const QAbstractItemModel *model, const QModelIndex &index)
    {
        int rows = model->rowCount(index);
        int child_rows = 0;
        for (int i = 0; i < rows; ++i) {
            child_rows += rowCount(model, model->index(i, 0, index));
        }
        return rows + child_rows;
    }

    FilterableTreeView m_tree;
    QPointer<KTextEditor::View> m_activeView;

Q_SIGNALS:
    void navigateLeftRight(int key);
};

enum BreadCrumbRole {
    PathRole = Qt::UserRole + 1,
    IsSeparator,
    IsSymbolCrumb,
};

class BreadCrumbDelegate : public QStyledItemDelegate
{
public:
    using QStyledItemDelegate::QStyledItemDelegate;

    static constexpr int separatorSize = 8; // Chevron decoration
    static constexpr int decorationSize = 16; // Icon inside a breadcrumb

    void initStyleOption(QStyleOptionViewItem *o, const QModelIndex &idx) const override
    {
        QStyledItemDelegate::initStyleOption(o, idx);
        o->decorationAlignment = Qt::AlignCenter;
        // We always want this icon size and nothing bigger
        if (idx.data(BreadCrumbRole::IsSeparator).toBool()) {
            o->decorationSize = QSize(separatorSize, separatorSize);
            o->viewItemPosition = QStyleOptionViewItem::ViewItemPosition::Middle;
        } else {
            o->decorationSize = QSize(decorationSize, decorationSize);
            o->viewItemPosition = QStyleOptionViewItem::ViewItemPosition::OnlyOne;
        }

        if (o->state & QStyle::State_MouseOver) {
            // No hover feedback for separators
            if (idx.data(BreadCrumbRole::IsSeparator).toBool()) {
                o->state.setFlag(QStyle::State_MouseOver, false);
                o->state.setFlag(QStyle::State_Active, false);
                o->state.setFlag(QStyle::State_HasFocus, false);
            } else {
                o->palette.setBrush(QPalette::Text, o->palette.windowText());
            }
        }
    }

    QSize sizeHint(const QStyleOptionViewItem &opt, const QModelIndex &idx) const override
    {
        if (idx.data(BreadCrumbRole::IsSeparator).toBool()) {
            const int margin = opt.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin);
            QSize s(separatorSize, separatorSize);
            s = s.grownBy({margin, 0, margin, 0});
            return s;
        }
        return QStyledItemDelegate::sizeHint(opt, idx);
    }
};

class BreadCrumbView : public QListView
{
    Q_OBJECT
public:
    BreadCrumbView(QWidget *parent, KateUrlBar *urlBar)
        : QListView(parent)
        , m_urlBar(urlBar)
    {
        setFlow(QListView::LeftToRight);
        setModel(&m_model);
        setFrameStyle(QFrame::NoFrame);
        setSelectionMode(QAbstractItemView::SingleSelection);
        setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        setItemDelegate(new BreadCrumbDelegate(this));
        setEditTriggers(QAbstractItemView::NoEditTriggers);
        setTextElideMode(Qt::ElideNone);
        setSpacing(0);

        connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, &BreadCrumbView::updatePalette, Qt::QueuedConnection);
        updatePalette();

        setContextMenuPolicy(Qt::CustomContextMenu);
        connect(this, &BreadCrumbView::customContextMenuRequested, this, &BreadCrumbView::onContextMenu);

        auto onConfigChanged = [this] {
            KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General"));
            auto v = cg.readEntry("Show Symbol In Navigation Bar", true);
            if (v != m_showSymbolCrumb) {
                m_showSymbolCrumb = v;
                Q_EMIT requestRefresh();
            }
        };
        connect(KateApp::self(), &KateApp::configurationChanged, this, onConfigChanged);
        onConfigChanged();

        connect(this, &QListView::clicked, this, &BreadCrumbView::onClicked);

        m_cursorChangedTimer.setSingleShot(true);
        m_cursorChangedTimer.setInterval(500ms);
        m_cursorChangedTimer.callOnTimeout(this, &BreadCrumbView::updateSymbolsCrumb);
    }

    void updatePalette()
    {
        auto pal = palette();
        pal.setBrush(QPalette::Base, qobject_cast<QWidget *>(parent())->palette().window());
        auto textColor = pal.windowText().color();
        textColor = textColor.lightness() > 127 ? textColor.darker(150) : textColor.lighter(150);
        pal.setBrush(QPalette::Inactive, QPalette::Text, textColor);
        pal.setBrush(QPalette::Active, QPalette::Text, textColor);
        setPalette(pal);
    }

    void setUrl(const QString &baseDir, const QUrl &url)
    {
        if (url.isEmpty()) {
            return;
        }

        const QString s = url.toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile);
        const std::vector<DirNamePath> &dirs = splittedUrl(baseDir, s);

        m_model.clear();
        if (m_symbolsModel) {
            m_symbolsModel->disconnect(this);
        }
        m_symbolsModel = nullptr;

        size_t i = 0;
        QIcon separator = m_urlBar->separator();
        for (const DirNamePath &dir : dirs) {
            auto item = new QStandardItem(dir.name);
            item->setData(dir.path, BreadCrumbRole::PathRole);
            m_model.appendRow(item);

            if (i < dirs.size() - 1) {
                auto sep = new QStandardItem(separator, {});
                sep->setSelectable(false);
                sep->setData(true, BreadCrumbRole::IsSeparator);
                m_model.appendRow(sep);
            } else {
                // last item, which is the filename, show icon with it
                const auto icon = QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(s, QMimeDatabase::MatchExtension).iconName());
                item->setIcon(icon);
            }
            i++;
        }

        if (!m_showSymbolCrumb) {
            return;
        }

        KateMainWindow *mainWindow = m_urlBar->viewManager()->mainWindow();
        QPointer<QObject> lsp = mainWindow->pluginView(QStringLiteral("lspclientplugin"));
        if (lsp) {
            addSymbolCrumb(lsp);
        }
    }

    void setDir(const QString &base)
    {
        m_model.clear();
        if (base.isEmpty()) {
            return;
        }

        QDir d(base);

        std::vector<DirNamePath> dirs;

        QString dirName = d.dirName();

        while (d.cdUp()) {
            if (dirName.isEmpty()) {
                continue;
            }
            dirs.push_back({dirName, d.absolutePath()});
            dirName = d.dirName();
        }
        std::reverse(dirs.begin(), dirs.end());

        QIcon separator = m_urlBar->separator();
        for (const BreadCrumbView::DirNamePath &dir : dirs) {
            auto item = new QStandardItem(dir.name);
            item->setData(dir.path, BreadCrumbRole::PathRole);
            m_model.appendRow(item);

            auto sep = new QStandardItem(separator, {});
            sep->setSelectable(false);
            sep->setData(true, BreadCrumbRole::IsSeparator);
            m_model.appendRow(sep);
        }
    }

    void addSymbolCrumb(QObject *lsp)
    {
        m_lastCursorLine = -1;
        QAbstractItemModel *model = nullptr;
        QMetaObject::invokeMethod(lsp, "documentSymbolsModel", Q_RETURN_ARG(QAbstractItemModel *, model));
        m_symbolsModel = model;
        if (!model) {
            return;
        }

        connect(m_symbolsModel, &QAbstractItemModel::modelReset, this, &BreadCrumbView::updateSymbolsCrumb);

        const auto view = m_urlBar->viewManager()->activeView();
        disconnect(m_connToView);
        if (view) {
            m_connToView = connect(view, &KTextEditor::View::cursorPositionChanged, this, [this](KTextEditor::View *, KTextEditor::Cursor pos) {
                if (pos.line() != m_lastCursorLine) {
                    // start if the line changed
                    m_lastCursorLine = pos.line();
                    m_cursorChangedTimer.start();
                }
            });
        }

        if (model->rowCount({}) == 0) {
            // the symbols are not ready
            return;
        }

        const auto idx = getSymbolCrumbText();
        if (!idx.isValid()) {
            return;
        }

        // Add separator
        auto sep = new QStandardItem(QIcon(m_urlBar->separator()), {});
        sep->setSelectable(false);
        sep->setData(true, BreadCrumbRole::IsSeparator);
        m_model.appendRow(sep);

        const auto icon = idx.data(Qt::DecorationRole).value<QIcon>();
        const auto text = idx.data().toString();
        auto *item = new QStandardItem(icon, text);
        item->setData(true, BreadCrumbRole::IsSymbolCrumb);
        m_model.appendRow(item);
    }

    void updateSymbolsCrumb()
    {
        QStandardItem *item = m_model.item(m_model.rowCount() - 1, 0);

        if (!m_urlBar->viewSpace()->isActiveSpace()) {
            if (item && item->data(BreadCrumbRole::IsSymbolCrumb).toBool()) {
                // we are not active viewspace, remove the symbol + separator from breadcrumb
                // This is important as LSP only gives us symbols for the current active view
                // which atm is in some other viewspace.
                // In future we might want to extend LSP to provide us models for documents
                // but for now this will do.
                qDeleteAll(m_model.takeRow(m_model.rowCount() - 1));
                qDeleteAll(m_model.takeRow(m_model.rowCount() - 1));
            }
            return;
        }

        const auto idx = getSymbolCrumbText();
        if (!idx.isValid()) {
            return;
        }

        if (!item || !item->data(BreadCrumbRole::IsSymbolCrumb).toBool()) {
            // Add separator
            auto sep = new QStandardItem(QIcon(m_urlBar->separator()), {});
            sep->setSelectable(false);
            sep->setData(true, BreadCrumbRole::IsSeparator);
            m_model.appendRow(sep);

            item = new QStandardItem;
            m_model.appendRow(item);
            item->setData(true, BreadCrumbRole::IsSymbolCrumb);
        }

        const auto text = idx.data().toString();
        const auto icon = idx.data(Qt::DecorationRole).value<QIcon>();
        item->setText(text);
        item->setIcon(icon);
    }

    QModelIndex getSymbolCrumbText()
    {
        if (!m_symbolsModel) {
            return {};
        }

        QModelIndex first = m_symbolsModel->index(0, 0);
        if (first.data(SymbolsTreeView::IsPlaceholder).toBool()) {
            return {};
        }

        const auto view = m_urlBar->viewSpace()->currentView();
        int line = view ? view->cursorPosition().line() : 0;

        QModelIndex idx;
        if (line > 0) {
            idx = SymbolsTreeView::symbolForCurrentLine(m_symbolsModel, idx, line);
        } else {
            idx = first;
        }

        if (!idx.isValid()) {
            idx = first;
        }
        return idx;
    }

    static bool IsSeparator(const QModelIndex &idx)
    {
        return idx.data(BreadCrumbRole::IsSeparator).toBool();
    }

    void keyPressEvent(QKeyEvent *ke) override
    {
        const auto key = ke->key();
        auto current = currentIndex();

        if (key == Qt::Key_Left || key == Qt::Key_Right) {
            onNavigateLeftRight(key, false);
        } else if (key == Qt::Key_Enter || key == Qt::Key_Return || key == Qt::Key_Down) {
            Q_EMIT clicked(current);
        } else if (key == Qt::Key_Escape) {
            Q_EMIT unsetFocus();
        }
    }

    void onClicked(const QModelIndex &idx)
    {
        // Clicked on the symbol?
        if (m_symbolsModel && idx.data(BreadCrumbRole::IsSymbolCrumb).toBool()) {
            auto activeView = m_urlBar->viewSpace()->currentView();
            if (!activeView) {
                // View must be there
                return;
            }
            SymbolsTreeView t(this);
            connect(
                &t,
                &SymbolsTreeView::navigateLeftRight,
                this,
                [this](int k) {
                    onNavigateLeftRight(k, true);
                },
                Qt::QueuedConnection);
            const QString symbolName = idx.data().toString();
            t.setSymbolsModel(m_symbolsModel, activeView, symbolName);
            const QPoint pos = mapToGlobal(rectForIndex(idx).bottomLeft());
            t.setFocus();
            t.exec(pos);
        }

        auto path = idx.data(BreadCrumbRole::PathRole).toString();
        if (path.isEmpty()) {
            return;
        }

        const QPoint pos = mapToGlobal(rectForIndex(idx).bottomLeft());

        m_isNavigating = true;

        QDir d(path);
        DirFilesList m(this);
        connect(&m, &DirFilesList::openUrl, m_urlBar, &KateUrlBar::openUrlRequested);
        connect(&m, &DirFilesList::openUrl, this, &BreadCrumbView::unsetFocus);
        connect(
            &m,
            &DirFilesList::navigateLeftRight,
            this,
            [this](int k) {
                onNavigateLeftRight(k, true);
            },
            Qt::QueuedConnection);
        m.setDir(d, idx.data().toString());
        m.setFocus();
        m.exec(pos);
        m_isNavigating = false;
    }

    void onContextMenu(const QPoint &pos)
    {
        if (selectedIndexes().empty()) {
            return;
        }
        QMenu menu = QMenu(viewport());
        const QModelIndex lastIndex = m_model.index(m_model.rowCount() - 1, 0);
        const bool lastIsSymbolCrumb = lastIndex.data(BreadCrumbRole::IsSymbolCrumb).toBool();

        QModelIndex selectedIndex = selectedIndexes().constFirst();
        auto path = selectedIndex.data(BreadCrumbRole::PathRole).toString();

        // -2 will be separator
        int lastUrlItem = lastIsSymbolCrumb ? m_model.rowCount() - 3 : m_model.rowCount() - 1;
        if (selectedIndex <= m_model.index(lastUrlItem, 0)) {
            const QString fileName = selectedIndex.data().toString();

            if (!path.isEmpty()) {
                menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy-path")), i18nc("@menu:action", "Copy Location"), this, [path, fileName] {
                    QGuiApplication::clipboard()->setText(QDir(path).absoluteFilePath(fileName));
                });
            }

            menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@menu:action", "Copy Filename"), this, [fileName] {
                QGuiApplication::clipboard()->setText(fileName);
            });
        } else {
            const QString item = selectedIndex.data().toString();

            menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@menu:action", "Copy Item"), this, [item] {
                QGuiApplication::clipboard()->setText(item);
            });
        }

        menu.exec(mapToGlobal(pos));
    }

    bool navigating() const
    {
        return m_isNavigating;
    }

    void openLastIndex()
    {
        const auto last = m_model.index(m_model.rowCount() - 1, 0);
        if (last.isValid()) {
            setCurrentIndex(last);
            Q_EMIT clicked(last);
        }
    }

    void updateSeparatorIcon()
    {
        auto newSeparator = m_urlBar->separator();
        for (int i = 0; i < m_model.rowCount(); ++i) {
            auto item = m_model.item(i);
            if (item && item->data(BreadCrumbRole::IsSeparator).toBool()) {
                item->setIcon(newSeparator);
            }
        }
    }

    int maxWidth() const
    {
        const auto rowCount = m_model.rowCount();
        int w = 0;
        QStyleOptionViewItem opt;
        initViewItemOption(&opt);
        auto delegate = itemDelegate();
        for (int i = 0; i < rowCount; ++i) {
            w += delegate->sizeHint(opt, m_model.index(i, 0)).width();
        }
        return w;
    }

private:
    QModelIndex lastIndex()
    {
        return m_model.index(m_model.rowCount() - 1, 0);
    }

    struct DirNamePath {
        QString name;
        QString path;
    };

    void onNavigateLeftRight(int key, bool open)
    {
        const auto current = currentIndex();
        const int step = IsSeparator(current) ? 1 : 2;
        const int nextRow = key == Qt::Key_Left ? current.row() - step : current.row() + step;
        auto nextIndex = current.sibling(nextRow, 0);
        if (nextIndex.isValid()) {
            setCurrentIndex(nextIndex);
            if (open) {
                Q_EMIT clicked(currentIndex());
            }
        }
    }

    static std::vector<DirNamePath> splittedUrl(const QString &base, const QString &s)
    {
        QDir dir(s);
        const QString fileName = dir.dirName();
        dir.cdUp();
        const QString path = dir.absolutePath();

        std::vector<DirNamePath> dirsList;
        dirsList.push_back(DirNamePath{.name = fileName, .path = path});

        // arrived at base?
        if (dir.absolutePath() == base) {
            return dirsList;
        }

        QString dirName = dir.dirName();

        while (dir.cdUp()) {
            if (dirName.isEmpty()) {
                continue;
            }

            DirNamePath dnp{.name = dirName, .path = dir.absolutePath()};
            dirsList.push_back(dnp);

            dirName = dir.dirName();

            // arrived at base?
            if (dir.absolutePath() == base) {
                break;
            }
        }
        std::reverse(dirsList.begin(), dirsList.end());
        return dirsList;
    }

    KateUrlBar *const m_urlBar;
    QStandardItemModel m_model;
    QPointer<QAbstractItemModel> m_symbolsModel;
    QMetaObject::Connection m_connToView; // Only one conn at a time
    bool m_isNavigating = false;
    bool m_showSymbolCrumb = true;
    int m_lastCursorLine = -1;
    QTimer m_cursorChangedTimer;

Q_SIGNALS:
    void unsetFocus();
    void requestRefresh();
};

// TODO: Merge this class back into KateUrlBar
class UrlbarContainer : public QWidget
{
public:
    UrlbarContainer(KateUrlBar *parent)
        : QWidget(parent)
        , m_urlBar(parent)
        , m_ellipses(new QToolButton(this))
        , m_baseCrumbView(new BreadCrumbView(this, parent))
        , m_mainCrumbView(new BreadCrumbView(this, parent))
    {
        m_ellipses->setText(QStringLiteral("…"));
        m_ellipses->setAutoRaise(true);
        auto urlBarLayout = new QHBoxLayout(this);
        connect(m_ellipses, &QToolButton::clicked, this, [urlBarLayout, this] {
            if (m_currBaseDir.isEmpty()) {
                qWarning("Unexpected empty base dir");
                return;
            }
            m_ellipses->hide();
            urlBarLayout->removeWidget(m_ellipses);
            m_baseCrumbView->setDir(m_currBaseDir);
            QSize s = m_baseCrumbView->sizeHint();
            s.setHeight(height());
            int w = m_baseCrumbView->maxWidth();
            s.setWidth(w);
            m_baseCrumbView->setFixedSize(s);
            urlBarLayout->insertWidget(0, m_baseCrumbView);
            m_baseCrumbView->show();
        });
        m_baseCrumbView->hide();
        m_baseCrumbView->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored);

        // UrlBar
        urlBarLayout->setSpacing(0);
        urlBarLayout->setContentsMargins({});
        urlBarLayout->addWidget(m_ellipses);
        urlBarLayout->addWidget(m_mainCrumbView);
        setFocusProxy(m_mainCrumbView);

        connect(m_mainCrumbView, &BreadCrumbView::unsetFocus, this, [this] {
            m_urlBar->viewManager()->activeView()->setFocus();
        });
        connect(m_mainCrumbView, &BreadCrumbView::requestRefresh, this, [this] {
            auto vs = m_urlBar->viewSpace();
            auto view = vs->currentView();
            if (vs && view) {
                setUrl(view->document());
            }
        });

        connect(
            KTextEditor::Editor::instance(),
            &KTextEditor::Editor::configChanged,
            this,
            [this] {
                m_sepPixmap = QPixmap();
                initSeparatorIcon();
                m_mainCrumbView->updateSeparatorIcon();
            },
            Qt::QueuedConnection);

        connect(&m_fullPathHideTimer, &QTimer::timeout, this, &UrlbarContainer::hideFullPath);
        m_fullPathHideTimer.setInterval(1s);
        m_fullPathHideTimer.setSingleShot(true);
    }

    void open()
    {
        if (m_mainCrumbView) {
            m_mainCrumbView->openLastIndex();
        }
    }

    void setUrl(KTextEditor::Document *doc)
    {
        const QString baseDir = Utils::projectBaseDirForDocument(doc);

        m_currBaseDir = baseDir;
        if (m_currBaseDir.isEmpty()) {
            m_ellipses->hide();
            m_baseCrumbView->hide();
        } else {
            m_ellipses->show();
        }
        m_mainCrumbView->setUrl(baseDir, doc->url());
    }

    QPixmap separatorPixmap()
    {
        if (m_sepPixmap.isNull()) {
            initSeparatorIcon();
        }
        return m_sepPixmap;
    }

protected:
    void leaveEvent(QEvent *e) override
    {
        if (!m_baseCrumbView->navigating()) {
            m_fullPathHideTimer.start();
        }
        QWidget::leaveEvent(e);
    }

    void enterEvent(QEnterEvent *e) override
    {
        m_fullPathHideTimer.stop();
        QWidget::enterEvent(e);
    }

private:
    void hideFullPath()
    {
        if (m_currBaseDir.isEmpty()) {
            return;
        }
        m_baseCrumbView->hide();
        layout()->removeWidget(m_baseCrumbView);
        static_cast<QHBoxLayout *>(layout())->insertWidget(0, m_ellipses);
        m_ellipses->show();
    }

    void initSeparatorIcon()
    {
        Q_ASSERT(m_sepPixmap.isNull());
        const auto dpr = this->devicePixelRatioF();
        m_sepPixmap = QPixmap(8 * dpr, 8 * dpr);
        m_sepPixmap.fill(Qt::transparent);

        auto pal = palette();
        auto textColor = pal.text().color();
        textColor = textColor.lightness() > 127 ? textColor.darker(150) : textColor.lighter(150);
        pal.setColor(QPalette::ButtonText, textColor);
        pal.setColor(QPalette::WindowText, textColor);
        pal.setColor(QPalette::Text, textColor);

        QPainter p(&m_sepPixmap);
        QStyleOption o;
        o.rect.setRect(0, 0, 8, 8);
        o.palette = pal;
        style()->drawPrimitive(QStyle::PE_IndicatorArrowRight, &o, &p, this);
        m_sepPixmap.setDevicePixelRatio(dpr);
    }

    KateUrlBar *const m_urlBar;
    QToolButton *const m_ellipses;
    BreadCrumbView *const m_baseCrumbView;
    BreadCrumbView *const m_mainCrumbView;
    QTimer m_fullPathHideTimer;
    QPixmap m_sepPixmap;
    QString m_currBaseDir;
};

KateUrlBar::KateUrlBar(KateViewSpace *parent)
    : QWidget(parent)
    , m_stack(new QStackedWidget(this))
    , m_urlBarView(new UrlbarContainer(this))
    , m_untitledDocLabel(new QLabel(this))
    , m_parentViewSpace(parent)
{
    setContentsMargins({});
    setFont(QApplication::font("QMenu")); // We don't want the default system font
    setFixedHeight(fontMetrics().height());

    setupLayout();

    auto *vm = parent->viewManager();
    connect(vm, &KateViewManager::viewChanged, this, &KateUrlBar::onViewChanged);

    connect(vm, &KateViewManager::showUrlNavBarChanged, this, [this, vm](bool show) {
        if (show && onViewChanged(vm->activeView())) {
            return;
        }
        setVisible(show);
        Q_EMIT maybeHiddenChanged();
    });

    setVisible(vm->showUrlNavBar());
}

void KateUrlBar::open()
{
    if (m_stack->currentWidget() == m_urlBarView) {
        m_urlBarView->open();
    }
}

KateViewManager *KateUrlBar::viewManager()
{
    return m_parentViewSpace->viewManager();
}

KateViewSpace *KateUrlBar::viewSpace()
{
    return m_parentViewSpace;
}

QIcon KateUrlBar::separator()
{
    return m_urlBarView->separatorPixmap();
}

void KateUrlBar::setupLayout()
{
    // Setup the stacked widget
    m_stack->addWidget(m_untitledDocLabel);
    m_stack->addWidget(m_urlBarView);

    // MainLayout
    auto *layout = new QHBoxLayout(this);
    layout->setContentsMargins({});
    layout->setSpacing(0);
    layout->addWidget(m_stack);

    auto updatePalette = [this]() {
        auto pal = m_untitledDocLabel->palette();
        auto textColor = pal.text().color();
        textColor = textColor.lightness() > 127 ? textColor.darker(150) : textColor.lighter(150);
        pal.setBrush(QPalette::Active, QPalette::WindowText, textColor);
        m_untitledDocLabel->setPalette(pal);
    };
    updatePalette();
    connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updatePalette, Qt::QueuedConnection);
}

bool KateUrlBar::onViewChanged(KTextEditor::View *v)
{
    // We are not active but we have a doc? => don't do anything
    // we check for a doc because we want to update the KateUrlBar
    // when kate starts
    if (!viewSpace()->isActiveSpace() && m_currentDoc) {
        return false;
    }

    if (!v) {
        updateForDocument(nullptr);
        // no view => show nothing
        m_untitledDocLabel->setText({});
        m_stack->setCurrentWidget(m_untitledDocLabel);
        setVisible(false);
        Q_EMIT maybeHiddenChanged();
        return true;
    }

    setVisible(viewManager()->showUrlNavBar());
    Q_EMIT maybeHiddenChanged();
    updateForDocument(v->document());
    return true;
}

void KateUrlBar::updateForDocument(KTextEditor::Document *doc)
{
    // always disconnect and perhaps set nullptr doc
    if (m_currentDoc) {
        disconnect(m_currentDoc, &KTextEditor::Document::documentNameChanged, this, &KateUrlBar::updateForDocument);
        disconnect(m_currentDoc, &KTextEditor::Document::documentUrlChanged, this, &KateUrlBar::updateForDocument);
    }
    m_currentDoc = doc;
    if (!doc) {
        return;
    }

    // we want to watch for name & url changed
    connect(m_currentDoc, &KTextEditor::Document::documentNameChanged, this, &KateUrlBar::updateForDocument);
    connect(m_currentDoc, &KTextEditor::Document::documentUrlChanged, this, &KateUrlBar::updateForDocument);

    if (m_currentDoc->url().isEmpty() || !m_currentDoc->url().isLocalFile()) {
        m_untitledDocLabel->setText(m_currentDoc->documentName());
        m_stack->setCurrentWidget(m_untitledDocLabel);
        return;
    }

    if (m_stack->currentWidget() != m_urlBarView) {
        m_stack->setCurrentWidget(m_urlBarView);
    }

    auto *vm = viewManager();
    if (vm && !vm->showUrlNavBar()) {
        return;
    }

    m_urlBarView->setUrl(doc);
}

#include "kateurlbar.moc"
#include "moc_kateurlbar.cpp"
