/*
 * Common code for MI debugger support
 *
 * Copyright 1999-2001 John Birch <jbb@kdevelop.org>
 * Copyright 2001 by Bernd Gehrmann <bernd@kdevelop.org>
 * Copyright 2006 Vladimir Prus <ghost@cs.msu.su>
 * Copyright 2007 Hamish Rodda <rodda@kde.org>
 * Copyright 2016  Aetf <aetf@unlimitedcodeworks.xyz>
 *
 * 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) version 3 or any later version
 * accepted by the membership of KDE e.V. (or its successor approved
 * by the membership of KDE e.V.), which shall act as a proxy
 * defined in Section 14 of version 3 of the license.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "midebuggerplugin.h"

#include "midebugjobs.h"
#include "dialogs/processselection.h"

#include <interfaces/context.h>
#include <interfaces/contextmenuextension.h>
#include <interfaces/icore.h>
#include <interfaces/idebugcontroller.h>
#include <interfaces/iruncontroller.h>
#include <interfaces/iuicontroller.h>
#include <language/interfaces/editorcontext.h>

#include <KActionCollection>
#include <KLocalizedString>
#include <KMessageBox>
#include <KParts/MainWindow>
#include <KStringHandler>

#include <QAction>
#include <QApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusServiceWatcher>
#include <QPointer>
#include <QSignalMapper>
#include <QTimer>

using namespace KDevelop;
using namespace KDevMI;

MIDebuggerPlugin::MIDebuggerPlugin(const QString &componentName, const QString& displayName, QObject *parent)
    : KDevelop::IPlugin(componentName, parent)
{
    core()->debugController()->initializeUi();

    setupActions(displayName);
    setupDBus();
}

void MIDebuggerPlugin::setupActions(const QString& displayName)
{
    KActionCollection* ac = actionCollection();

    QAction * action = new QAction(this);
    action->setIcon(QIcon::fromTheme(QStringLiteral("core")));
    action->setText(i18n("Examine Core File with %1", displayName));
    action->setWhatsThis(i18n("<b>Examine core file</b>"
                              "<p>This loads a core file, which is typically created "
                              "after the application has crashed, e.g. with a "
                              "segmentation fault. The core file contains an "
                              "image of the program memory at the time it crashed, "
                              "allowing you to do a post-mortem analysis.</p>"));
    connect(action, &QAction::triggered, this, &MIDebuggerPlugin::slotExamineCore);
    ac->addAction(QStringLiteral("debug_core"), action);

#if KF5SysGuard_FOUND
    action = new QAction(this);
    action->setIcon(QIcon::fromTheme(QStringLiteral("connect_creating")));
    action->setText(i18n("Attach to Process with %1", displayName));
    action->setWhatsThis(i18n("<b>Attach to process</b>"
                              "<p>Attaches the debugger to a running process.</p>"));
    connect(action, &QAction::triggered, this, &MIDebuggerPlugin::slotAttachProcess);
    ac->addAction(QStringLiteral("debug_attach"), action);
#endif
}

void MIDebuggerPlugin::setupDBus()
{
    m_drkonqiMap = new QSignalMapper(this);
    connect(m_drkonqiMap, static_cast<void(QSignalMapper::*)(QObject*)>(&QSignalMapper::mapped),
            this, &MIDebuggerPlugin::slotDebugExternalProcess);

    QDBusConnectionInterface* dbusInterface = QDBusConnection::sessionBus().interface();
    for (const auto &service : dbusInterface->registeredServiceNames().value()) {
        slotDBusServiceRegistered(service);
    }

    QDBusServiceWatcher* watcher = new QDBusServiceWatcher(this);
    connect(watcher, &QDBusServiceWatcher::serviceRegistered,
            this, &MIDebuggerPlugin::slotDBusServiceRegistered);
    connect(watcher, &QDBusServiceWatcher::serviceUnregistered,
            this, &MIDebuggerPlugin::slotDBusServiceUnregistered);
}

void MIDebuggerPlugin::unload()
{
    unloadToolviews();
}

MIDebuggerPlugin::~MIDebuggerPlugin()
{
}

void MIDebuggerPlugin::slotDBusServiceRegistered(const QString& service)
{
    if (service.startsWith(QLatin1String("org.kde.drkonqi"))) {
        // New registration
        QDBusInterface* drkonqiInterface = new QDBusInterface(service, QStringLiteral("/krashinfo"),
                                                              QString(), QDBusConnection::sessionBus(),
                                                              this);
        m_drkonqis.insert(service, drkonqiInterface);

        connect(drkonqiInterface, SIGNAL(acceptDebuggingApplication()), m_drkonqiMap, SLOT(map()));
        m_drkonqiMap->setMapping(drkonqiInterface, drkonqiInterface);

        drkonqiInterface->call(QStringLiteral("registerDebuggingApplication"), i18n("KDevelop"));
    }
}

void MIDebuggerPlugin::slotDBusServiceUnregistered(const QString& service)
{
    if (service.startsWith(QLatin1String("org.kde.drkonqi"))) {
        // Deregistration
        if (m_drkonqis.contains(service))
            delete m_drkonqis.take(service);
    }
}

void MIDebuggerPlugin::slotDebugExternalProcess(QObject* interface)
{
    auto dbusInterface = static_cast<QDBusInterface*>(interface);

    QDBusReply<int> reply = dbusInterface->call(QStringLiteral("pid"));
    if (reply.isValid()) {
        attachProcess(reply.value());
        QTimer::singleShot(500, this, &MIDebuggerPlugin::slotCloseDrKonqi);

        m_drkonqi = m_drkonqis.key(dbusInterface);
    }

    core()->uiController()->activeMainWindow()->raise();
}

void MIDebuggerPlugin::slotCloseDrKonqi()
{
    if (!m_drkonqi.isEmpty()) {
        QDBusInterface drkonqiInterface(m_drkonqi, QStringLiteral("/MainApplication"), QStringLiteral("org.kde.KApplication"));
        drkonqiInterface.call(QStringLiteral("quit"));
        m_drkonqi.clear();
    }
}

ContextMenuExtension MIDebuggerPlugin::contextMenuExtension(Context* context, QWidget* parent)
{
    ContextMenuExtension menuExt = IPlugin::contextMenuExtension(context, parent);

    if (context->type() != KDevelop::Context::EditorContext)
        return menuExt;

    EditorContext *econtext = dynamic_cast<EditorContext*>(context);
    if (!econtext)
        return menuExt;

    QString contextIdent = econtext->currentWord();

    if (!contextIdent.isEmpty())
    {
        QString squeezed = KStringHandler::csqueeze(contextIdent, 30);

        QAction* action = new QAction(parent);
        action->setText(i18n("Evaluate: %1", squeezed));
        action->setWhatsThis(i18n("<b>Evaluate expression</b>"
                                  "<p>Shows the value of the expression under the cursor.</p>"));
        connect(action, &QAction::triggered, this, [this, contextIdent](){
            emit addWatchVariable(contextIdent);
        });
        menuExt.addAction(ContextMenuExtension::DebugGroup, action);

        action = new QAction(parent);
        action->setText(i18n("Watch: %1", squeezed));
        action->setWhatsThis(i18n("<b>Watch expression</b>"
                                  "<p>Adds the expression under the cursor to the Variables/Watch list.</p>"));
        connect(action, &QAction::triggered, this, [this, contextIdent](){
            emit evaluateExpression(contextIdent);
        });
        menuExt.addAction(ContextMenuExtension::DebugGroup, action);
    }

    return menuExt;
}

void MIDebuggerPlugin::slotExamineCore()
{
    showStatusMessage(i18n("Choose a core file to examine..."), 1000);

    if (core()->debugController()->currentSession() != nullptr) {
        KMessageBox::ButtonCode answer = KMessageBox::warningYesNo(
            core()->uiController()->activeMainWindow(),
            i18n("A program is already being debugged. Do you want to abort the "
                 "currently running debug session and continue?"));
        if (answer == KMessageBox::No)
            return;
    }
    MIExamineCoreJob *job = new MIExamineCoreJob(this, core()->runController());
    core()->runController()->registerJob(job);
    // job->start() is called in registerJob
}

#if KF5SysGuard_FOUND
void MIDebuggerPlugin::slotAttachProcess()
{
    showStatusMessage(i18n("Choose a process to attach to..."), 1000);

    if (core()->debugController()->currentSession() != nullptr) {
        KMessageBox::ButtonCode answer = KMessageBox::warningYesNo(
            core()->uiController()->activeMainWindow(),
            i18n("A program is already being debugged. Do you want to abort the "
                 "currently running debug session and continue?"));
        if (answer == KMessageBox::No)
            return;
    }

    QPointer<ProcessSelectionDialog> dlg = new ProcessSelectionDialog(core()->uiController()->activeMainWindow());
    if (!dlg->exec() || !dlg->pidSelected()) {
        delete dlg;
        return;
    }

    // TODO: move check into process selection dialog
    int pid = dlg->pidSelected();
    delete dlg;
    if (QApplication::applicationPid() == pid)
        KMessageBox::error(core()->uiController()->activeMainWindow(),
                           i18n("Not attaching to process %1: cannot attach the debugger to itself.", pid));
    else
        attachProcess(pid);
}
#endif

void MIDebuggerPlugin::attachProcess(int pid)
{
    MIAttachProcessJob *job = new MIAttachProcessJob(this, pid, core()->runController());
    core()->runController()->registerJob(job);
    // job->start() is called in registerJob
}

QString MIDebuggerPlugin::statusName() const
{
    return i18n("Debugger");
}

void MIDebuggerPlugin::showStatusMessage(const QString& msg, int timeout)
{
    emit showMessage(this, msg, timeout);
}
