/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "languageclientoutline.h"

#include "languageclientmanager.h"
#include "languageclientutils.h"

#include <coreplugin/find/itemviewfind.h>
#include <coreplugin/editormanager/ieditor.h>
#include <languageserverprotocol/languagefeatures.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <utils/itemviews.h>
#include <utils/mimetypes/mimedatabase.h>
#include <utils/treemodel.h>
#include <utils/treeviewcombobox.h>
#include <utils/utilsicons.h>

#include <QBoxLayout>

using namespace LanguageServerProtocol;

namespace LanguageClient {

class LanguageClientOutlineItem : public Utils::TypedTreeItem<LanguageClientOutlineItem>
{
public:
    LanguageClientOutlineItem() = default;
    LanguageClientOutlineItem(const SymbolInformation &info)
        : m_name(info.name())
        , m_range(info.location().range())
        , m_type(info.kind())
    { }

    LanguageClientOutlineItem(const DocumentSymbol &info, const SymbolStringifier &stringifier)
        : m_name(info.name())
        , m_detail(info.detail().value_or(QString()))
        , m_range(info.range())
        , m_symbolStringifier(stringifier)
        , m_type(info.kind())
    {
        for (const DocumentSymbol &child : info.children().value_or(QList<DocumentSymbol>()))
            appendChild(new LanguageClientOutlineItem(child, stringifier));
    }

    // TreeItem interface
    QVariant data(int column, int role) const override
    {
        switch (role) {
        case Qt::DecorationRole:
            return symbolIcon(m_type);
        case Qt::DisplayRole:
            return m_symbolStringifier
                    ? m_symbolStringifier(static_cast<SymbolKind>(m_type), m_name, m_detail)
                    : m_name;
        default:
            return Utils::TreeItem::data(column, role);
        }
    }

    Range range() const { return m_range; }
    Position pos() const { return m_range.start(); }
    bool contains(const Position &pos) const { return m_range.contains(pos); }

private:
    QString m_name;
    QString m_detail;
    Range m_range;
    SymbolStringifier m_symbolStringifier;
    int m_type = -1;
};

class LanguageClientOutlineModel : public Utils::TreeModel<LanguageClientOutlineItem>
{
public:
    using Utils::TreeModel<LanguageClientOutlineItem>::TreeModel;
    void setInfo(const QList<SymbolInformation> &info)
    {
        clear();
        for (const SymbolInformation &symbol : info)
            rootItem()->appendChild(new LanguageClientOutlineItem(symbol));
    }
    void setInfo(const QList<DocumentSymbol> &info)
    {
        clear();
        for (const DocumentSymbol &symbol : info)
            rootItem()->appendChild(new LanguageClientOutlineItem(symbol, m_symbolStringifier));
    }

    void setSymbolStringifier(const SymbolStringifier &stringifier)
    {
        m_symbolStringifier = stringifier;
    }

private:
    SymbolStringifier m_symbolStringifier;
};

class LanguageClientOutlineWidget : public TextEditor::IOutlineWidget
{
public:
    LanguageClientOutlineWidget(Client *client, TextEditor::BaseTextEditor *editor);

    // IOutlineWidget interface
public:
    QList<QAction *> filterMenuActions() const override;
    void setCursorSynchronization(bool syncWithCursor) override;

private:
    void handleResponse(const DocumentUri &uri, const DocumentSymbolsResult &response);
    void updateTextCursor(const QModelIndex &proxyIndex);
    void updateSelectionInTree(const QTextCursor &currentCursor);
    void onItemActivated(const QModelIndex &index);

    QPointer<Client> m_client;
    QPointer<TextEditor::BaseTextEditor> m_editor;
    LanguageClientOutlineModel m_model;
    Utils::TreeView m_view;
    DocumentUri m_uri;
    bool m_sync = false;
};

LanguageClientOutlineWidget::LanguageClientOutlineWidget(Client *client,
                                                         TextEditor::BaseTextEditor *editor)
    : m_client(client)
    , m_editor(editor)
    , m_view(this)
    , m_uri(DocumentUri::fromFilePath(editor->textDocument()->filePath()))
{
    connect(client->documentSymbolCache(),
            &DocumentSymbolCache::gotSymbols,
            this,
            &LanguageClientOutlineWidget::handleResponse);
    connect(client, &Client::documentUpdated, this, [this](TextEditor::TextDocument *document) {
        if (m_client && m_uri == DocumentUri::fromFilePath(document->filePath()))
            m_client->documentSymbolCache()->requestSymbols(m_uri, Schedule::Delayed);
    });

    client->documentSymbolCache()->requestSymbols(m_uri, Schedule::Delayed);

    auto *layout = new QVBoxLayout;
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(0);
    layout->addWidget(Core::ItemViewFind::createSearchableWrapper(&m_view));
    setLayout(layout);
    m_model.setSymbolStringifier(m_client->symbolStringifier());
    m_view.setModel(&m_model);
    m_view.setHeaderHidden(true);
    m_view.setExpandsOnDoubleClick(false);
    connect(&m_view, &QAbstractItemView::activated,
            this, &LanguageClientOutlineWidget::onItemActivated);
    connect(m_editor->editorWidget(), &TextEditor::TextEditorWidget::cursorPositionChanged,
            this, [this](){
        if (m_sync)
            updateSelectionInTree(m_editor->textCursor());
    });
}

QList<QAction *> LanguageClientOutlineWidget::filterMenuActions() const
{
    return {};
}

void LanguageClientOutlineWidget::setCursorSynchronization(bool syncWithCursor)
{
    m_sync = syncWithCursor;
    if (m_sync && m_editor)
        updateSelectionInTree(m_editor->textCursor());
}

void LanguageClientOutlineWidget::handleResponse(const DocumentUri &uri,
                                                 const DocumentSymbolsResult &result)
{
    if (uri != m_uri)
        return;
    if (Utils::holds_alternative<QList<SymbolInformation>>(result))
        m_model.setInfo(Utils::get<QList<SymbolInformation>>(result));
    else if (Utils::holds_alternative<QList<DocumentSymbol>>(result))
        m_model.setInfo(Utils::get<QList<DocumentSymbol>>(result));
    else
        m_model.clear();

    // The list has changed, update the current items
    updateSelectionInTree(m_editor->textCursor());
}

void LanguageClientOutlineWidget::updateTextCursor(const QModelIndex &proxyIndex)
{
    LanguageClientOutlineItem *item = m_model.itemForIndex(proxyIndex);
    const Position &pos = item->pos();
    // line has to be 1 based, column 0 based!
    m_editor->editorWidget()->gotoLine(pos.line() + 1, pos.character(), true, true);
}

void LanguageClientOutlineWidget::updateSelectionInTree(const QTextCursor &currentCursor)
{
    QItemSelection selection;
    const Position pos(currentCursor);
    m_model.forAllItems([&](const LanguageClientOutlineItem *item) {
        if (item->contains(pos))
            selection.select(m_model.indexForItem(item), m_model.indexForItem(item));
    });
    m_view.selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect);
    if (!selection.isEmpty())
        m_view.scrollTo(selection.indexes().first());
}

void LanguageClientOutlineWidget::onItemActivated(const QModelIndex &index)
{
    if (!index.isValid() || !m_editor)
        return;

    updateTextCursor(index);
    m_editor->widget()->setFocus();
}

bool LanguageClientOutlineWidgetFactory::clientSupportsDocumentSymbols(
    const Client *client, const TextEditor::TextDocument *doc)
{
    if (!client)
        return false;
    DynamicCapabilities dc = client->dynamicCapabilities();
    if (dc.isRegistered(DocumentSymbolsRequest::methodName).value_or(false)) {
        TextDocumentRegistrationOptions options(dc.option(DocumentSymbolsRequest::methodName));
        return !options.isValid()
               || options.filterApplies(doc->filePath(), Utils::mimeTypeForName(doc->mimeType()));
    }
    const Utils::optional<Utils::variant<bool, WorkDoneProgressOptions>> &provider
        = client->capabilities().documentSymbolProvider();
    if (!provider.has_value())
        return false;
    if (Utils::holds_alternative<bool>(*provider))
        return Utils::get<bool>(*provider);
    return true;
}

bool LanguageClientOutlineWidgetFactory::supportsEditor(Core::IEditor *editor) const
{
    auto doc = qobject_cast<TextEditor::TextDocument *>(editor->document());
    if (!doc)
        return false;
    return clientSupportsDocumentSymbols(LanguageClientManager::clientForDocument(doc), doc);
}

TextEditor::IOutlineWidget *LanguageClientOutlineWidgetFactory::createWidget(Core::IEditor *editor)
{
    auto textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
    QTC_ASSERT(textEditor, return nullptr);
    Client *client = LanguageClientManager::clientForDocument(textEditor->textDocument());
    if (!client || !clientSupportsDocumentSymbols(client, textEditor->textDocument()))
        return nullptr;
    return new LanguageClientOutlineWidget(client, textEditor);
}

class OutlineComboBox : public Utils::TreeViewComboBox
{
public:
    OutlineComboBox(Client *client, TextEditor::BaseTextEditor *editor);

private:
    void updateModel(const DocumentUri &resultUri, const DocumentSymbolsResult &result);
    void updateEntry();
    void activateEntry();
    void documentUpdated(TextEditor::TextDocument *document);

    LanguageClientOutlineModel m_model;
    QPointer<Client> m_client;
    TextEditor::TextEditorWidget *m_editorWidget;
    const DocumentUri m_uri;
};

Utils::TreeViewComboBox *LanguageClientOutlineWidgetFactory::createComboBox(Client *client,
                                                                            Core::IEditor *editor)
{
    auto textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
    QTC_ASSERT(textEditor, return nullptr);
    TextEditor::TextDocument *document = textEditor->textDocument();
    if (!client || !clientSupportsDocumentSymbols(client, document))
        return nullptr;

    return new OutlineComboBox(client, textEditor);
}

OutlineComboBox::OutlineComboBox(Client *client, TextEditor::BaseTextEditor *editor)
    : m_client(client)
    , m_editorWidget(editor->editorWidget())
    , m_uri(DocumentUri::fromFilePath(editor->document()->filePath()))
{
    m_model.setSymbolStringifier(client->symbolStringifier());
    setModel(&m_model);
    setMinimumContentsLength(13);
    QSizePolicy policy = sizePolicy();
    policy.setHorizontalPolicy(QSizePolicy::Expanding);
    setSizePolicy(policy);
    setMaxVisibleItems(40);

    connect(client->documentSymbolCache(), &DocumentSymbolCache::gotSymbols,
            this, &OutlineComboBox::updateModel);
    connect(client, &Client::documentUpdated, this, &OutlineComboBox::documentUpdated);
    connect(m_editorWidget, &TextEditor::TextEditorWidget::cursorPositionChanged,
            this, &OutlineComboBox::updateEntry);
    connect(this, QOverload<int>::of(&QComboBox::activated), this, &OutlineComboBox::activateEntry);

    documentUpdated(editor->textDocument());
}

void OutlineComboBox::updateModel(const DocumentUri &resultUri, const DocumentSymbolsResult &result)
{
    if (m_uri != resultUri)
        return;
    if (Utils::holds_alternative<QList<SymbolInformation>>(result))
        m_model.setInfo(Utils::get<QList<SymbolInformation>>(result));
    else if (Utils::holds_alternative<QList<DocumentSymbol>>(result))
        m_model.setInfo(Utils::get<QList<DocumentSymbol>>(result));
    else
        m_model.clear();

    view()->expandAll();
    // The list has changed, update the current item
    updateEntry();
}

void OutlineComboBox::updateEntry()
{
    const Position pos(m_editorWidget->textCursor());
    LanguageClientOutlineItem *itemForCursor = nullptr;
    m_model.forAllItems([&](LanguageClientOutlineItem *candidate){
        if (!candidate->contains(pos))
            return;
        if (itemForCursor && candidate->range().contains(itemForCursor->range()))
            return; // skip item if the range is equal or bigger than the previous found range
        itemForCursor = candidate;
    });
    if (itemForCursor)
        setCurrentIndex(m_model.indexForItem(itemForCursor));
}

void OutlineComboBox::activateEntry()
{
    const QModelIndex modelIndex = view()->currentIndex();
    if (modelIndex.isValid()) {
        const Position &pos = m_model.itemForIndex(modelIndex)->pos();
        Core::EditorManager::cutForwardNavigationHistory();
        Core::EditorManager::addCurrentPositionToNavigationHistory();
        // line has to be 1 based, column 0 based!
        m_editorWidget->gotoLine(pos.line() + 1, pos.character(), true, true);
        emit m_editorWidget->activateEditor();
    }
}

void OutlineComboBox::documentUpdated(TextEditor::TextDocument *document)
{
    if (document == m_editorWidget->textDocument())
        m_client->documentSymbolCache()->requestSymbols(m_uri, Schedule::Delayed);
}

} // namespace LanguageClient
