#include "DebugActions.h"
#include "common/Configuration.h"
#include "common/Helpers.h"
#include "common/ShortcutManager.h"
#include "common/TextEditDialog.h"
#include "core/MainWindow.h"

#ifdef IAITO_ENABLE_DEBUGGER
#include "dialogs/AttachProcDialog.h"
#include "dialogs/NativeDebugDialog.h"
#include "dialogs/RemoteDebugDialog.h"
#endif

#include <QDialogButtonBox>
#include <QDir>
#include <QFileInfo>
#include <QInputDialog>
#include <QList>
#include <QMenu>
#include <QPainter>
#include <QPushButton>
#include <QSettings>
#include <QStandardPaths>
#include <QStyle>
#include <QTextEdit>
#include <QToolBar>
#include <QToolButton>
#include <QVBoxLayout>

static void makeBorderlessToolbarButton(QToolButton *button)
{
    button->setAutoRaise(true);
    button->setStyleSheet(QStringLiteral(
        "QToolButton { border: 0; background: transparent; }"
        "QToolButton::menu-button { border: 0; }"));
}

static QIcon standardToolbarIcon(
    const QToolBar *toolBar, QStyle::StandardPixmap pixmap, const QString &fallbackPath)
{
    QIcon icon;
    if (toolBar && toolBar->style()) {
        icon = toolBar->style()->standardIcon(pixmap, nullptr, toolBar);
    }
    return icon.isNull() ? QIcon(fallbackPath) : icon;
}

DebugActions::DebugActions(QToolBar *toolBar, MainWindow *main)
    : QObject(main)
    , toolBar(toolBar)
    , main(main)
{
    setObjectName("DebugActions");
    // setIconSize(QSize(16, 16));

    // define icons
    refreshStandardIcons();

    // define action labels
    QString startEmulLabel = tr("Start emulation");
    QString startAttachLabel = tr("Attach to process");
    QString startRemoteLabel = tr("Connect to a remote debugger");
    QString stopDebugLabel = tr("Stop debug");
    QString stopEmulLabel = tr("Stop emulation");
    QString restartEmulLabel = tr("Restart emulation");
    QString continueUMLabel = tr("Continue until main");
    QString continueUCLabel = tr("Continue until call");
    QString continueUSLabel = tr("Continue until syscall");
    QString continueUALabel = tr("Continue until address");
    QString continueUKLabel = tr("Continue until signal");
    QString continueUPLabel = tr("Continue until program");
    QString continueURLabel = tr("Continue until ret");
    QString stepLabel = tr("Step");
    QString stepOverLabel = tr("Step over");
    QString stepOutLabel = tr("Step out");
    suspendLabel = tr("Suspend the process");
    continueLabel = tr("Continue");
    restartDebugLabel = tr("Restart program");
    startDebugLabel = tr("Start debug");
    rarunProfileLabel = tr("Rarun profile");

    // define actions
    rarunProfile = new QAction(rarunProfileLabel, this);

    actionStart = new QAction(startDebugIcon, startDebugLabel, this);
    ShortcutMgr()->bindAction("debug.start", actionStart);
    actionStartEmul = new QAction(startEmulIcon, startEmulLabel, this);
    actionAttach = new QAction(startAttachIcon, startAttachLabel, this);
    actionStartRemote = new QAction(startRemoteIcon, startRemoteLabel, this);
    actionStop = new QAction(stopIcon, stopDebugLabel, this);
    actionContinue = new QAction(continueIcon, continueLabel, this);
    ShortcutMgr()->bindAction("debug.continue", actionContinue);
    actionContinueUntilMain = new QAction(continueUMLabel, this);
    actionContinueUntilCall = new QAction(continueUCLabel, this);
    actionContinueUntilSyscall = new QAction(continueUSLabel, this);
    actionContinueUntilAddress = new QAction(continueUALabel, this);
    actionContinueUntilSignal = new QAction(continueUKLabel, this);
    actionContinueUntilProgram = new QAction(continueUPLabel, this);
    actionContinueUntilRet = new QAction(continueURLabel, this);
    actionStep = new QAction(stepLabel, this);
    ShortcutMgr()->bindAction("debug.stepInto", actionStep);
    actionStepOver = new QAction(stepOverLabel, this);
    ShortcutMgr()->bindAction("debug.stepOver", actionStepOver);
    actionStepOut = new QAction(stepOutLabel, this);
    ShortcutMgr()->bindAction("debug.stepOut", actionStepOut);

    QToolButton *startButton = new QToolButton;
    startButton->setObjectName(QStringLiteral("debugStartButton"));
    makeBorderlessToolbarButton(startButton);
    startButton->setPopupMode(QToolButton::MenuButtonPopup);
    connect(startButton, &QToolButton::triggered, startButton, &QToolButton::setDefaultAction);
    QMenu *startMenu = new QMenu(startButton);

    // only emulation is currently allowed
#ifdef IAITO_ENABLE_DEBUGGER
    startMenu->addAction(actionStart);
#endif
    startMenu->addAction(actionStartEmul);
#ifdef IAITO_ENABLE_DEBUGGER
    startMenu->addAction(actionAttach);
    startMenu->addAction(actionStartRemote);
    startButton->setDefaultAction(actionStart);
#else
    actionStart->setVisible(false);
    actionAttach->setVisible(false);
    actionStartRemote->setVisible(false);
    startButton->setDefaultAction(actionStartEmul);
#endif
    startButton->setMenu(startMenu);

    continueUntilButton = new QToolButton;
    continueUntilButton->setObjectName(QStringLiteral("debugContinueUntilButton"));
    makeBorderlessToolbarButton(continueUntilButton);
    continueUntilButton->setPopupMode(QToolButton::MenuButtonPopup);
    connect(
        continueUntilButton,
        &QToolButton::triggered,
        continueUntilButton,
        &QToolButton::setDefaultAction);
    QMenu *continueUntilMenu = new QMenu(continueUntilButton);
    continueUntilMenu->addAction(actionContinueUntilMain);
    continueUntilMenu->addAction(actionContinueUntilCall);
    continueUntilMenu->addAction(actionContinueUntilSyscall);
    continueUntilMenu->addAction(actionContinueUntilAddress);
    continueUntilMenu->addAction(actionContinueUntilSignal);
    continueUntilMenu->addAction(actionContinueUntilProgram);
    continueUntilMenu->addAction(actionContinueUntilRet);
    continueUntilButton->setMenu(continueUntilMenu);
    continueUntilButton->setDefaultAction(actionContinueUntilMain);

    // define toolbar widgets and actions
    toolBar->addWidget(startButton);
    toolBar->addAction(actionContinue);
    toolBar->addAction(actionStop);
    actionAllContinues = toolBar->addWidget(continueUntilButton);
    toolBar->addAction(actionStepOver);
    toolBar->addAction(actionStep);
    toolBar->addAction(actionStepOut);

    allActions
        = {actionStop,
           actionAllContinues,
           actionContinue,
           actionContinueUntilCall,
           actionContinueUntilMain,
           actionContinueUntilSyscall,
           actionContinueUntilAddress,
           actionContinueUntilSignal,
           actionContinueUntilProgram,
           actionContinueUntilRet,
           actionStep,
           actionStepOut,
           actionStepOver};
    // hide allactions
    setAllActionsVisible(false);

    // Toggle all buttons except restart, suspend(=continue) and stop since
    // those are necessary to avoid staying stuck
    toggleActions
        = {actionStepOver,
           actionStep,
           actionStepOut,
           actionContinueUntilMain,
           actionContinueUntilCall,
           actionContinueUntilSyscall,
           actionContinueUntilAddress,
           actionContinueUntilSignal,
           actionContinueUntilProgram,
           actionContinueUntilRet};
#ifdef IAITO_ENABLE_DEBUGGER
    toggleConnectionActions = {actionAttach, actionStartRemote};
#endif

    connect(Core(), &IaitoCore::debugProcessFinished, this, [](int pid) {
        QMessageBox msgBox;
        msgBox.setText(tr("Debugged process exited (") + QString::number(pid) + ")");
        msgBox.exec();
    });

    // When a debug session is started externally (e.g. via -d flag), the
    // toolbar must switch to the "attached" state showing step/continue/stop.
    connect(Core(), &IaitoCore::toggleDebugView, this, [=, this]() {
        if (Core()->currentlyDebugging) {
            setAllActionsVisible(true);
#ifdef IAITO_ENABLE_DEBUGGER
            actionStart->setVisible(false);
            actionStartRemote->setVisible(false);
#endif
            actionStartEmul->setVisible(false);
            actionStop->setText(tr("Detach from process"));
            actionStop->setIcon(detachIcon);
        }
    });

    connect(Core(), &IaitoCore::debugTaskStateChanged, this, [=, this]() {
        bool disableToolbar = Core()->isDebugTaskInProgress();
        if (Core()->currentlyDebugging) {
            for (QAction *a : toggleActions) {
                a->setDisabled(disableToolbar);
            }
            // Suspend should only be available when other icons are disabled
            if (disableToolbar) {
                actionContinue->setText(suspendLabel);
                actionContinue->setIcon(suspendIcon);
            } else {
                actionContinue->setText(continueLabel);
                actionContinue->setIcon(continueIcon);
            }
        } else {
            for (QAction *a : toggleConnectionActions) {
                a->setDisabled(disableToolbar);
            }
        }
    });

    connect(actionStop, &QAction::triggered, Core(), &IaitoCore::stopDebug);
    connect(actionStop, &QAction::triggered, [=, this]() {
#ifdef IAITO_ENABLE_DEBUGGER
        actionStart->setVisible(true);
        actionAttach->setVisible(true);
        actionStartRemote->setVisible(true);
        actionStart->setText(startDebugLabel);
        actionStart->setIcon(startDebugIcon);
#endif
        actionStartEmul->setVisible(true);
        actionStop->setText(stopDebugLabel);
        actionStop->setIcon(stopIcon);
        actionStartEmul->setText(startEmulLabel);
        actionStartEmul->setIcon(startEmulIcon);
        continueUntilButton->setDefaultAction(actionContinueUntilMain);
        setAllActionsVisible(false);
    });
    connect(actionStep, &QAction::triggered, Core(), &IaitoCore::stepDebug);
#ifdef IAITO_ENABLE_DEBUGGER
    connect(actionStart, &QAction::triggered, this, &DebugActions::startDebug);
#endif
    connect(rarunProfile, &QAction::triggered, this, &DebugActions::editRarunProfile);

#ifdef IAITO_ENABLE_DEBUGGER
    connect(actionAttach, &QAction::triggered, this, &DebugActions::attachProcessDialog);
    connect(actionStartRemote, &QAction::triggered, this, &DebugActions::attachRemoteDialog);
    connect(Core(), &IaitoCore::attachedRemote, this, &DebugActions::onAttachedRemoteDebugger);
#endif
    connect(actionStartEmul, &QAction::triggered, Core(), &IaitoCore::startEmulation);
    connect(actionStartEmul, &QAction::triggered, [=, this]() {
        setAllActionsVisible(true);
#ifdef IAITO_ENABLE_DEBUGGER
        actionStart->setVisible(false);
        actionAttach->setVisible(false);
        actionStartRemote->setVisible(false);
#endif
        actionContinueUntilMain->setVisible(false);
        actionStepOut->setVisible(false);
        continueUntilButton->setDefaultAction(actionContinueUntilSyscall);
        actionStartEmul->setText(restartEmulLabel);
        actionStartEmul->setIcon(restartIcon);
        actionStop->setText(stopEmulLabel);
    });
    connect(actionStepOver, &QAction::triggered, Core(), &IaitoCore::stepOverDebug);
    connect(actionStepOut, &QAction::triggered, Core(), &IaitoCore::stepOutDebug);
    connect(actionContinueUntilMain, &QAction::triggered, this, &DebugActions::continueUntilMain);
    connect(actionContinueUntilCall, &QAction::triggered, Core(), &IaitoCore::continueUntilCall);
    connect(actionContinueUntilSyscall, &QAction::triggered, Core(), &IaitoCore::continueUntilSyscall);
    connect(
        actionContinueUntilAddress, &QAction::triggered, this, &DebugActions::continueUntilAddress);
    connect(actionContinueUntilSignal, &QAction::triggered, this, &DebugActions::continueUntilSignal);
    connect(actionContinueUntilProgram, &QAction::triggered, Core(), &IaitoCore::continueUntilProgram);
    connect(actionContinueUntilRet, &QAction::triggered, Core(), &IaitoCore::continueUntilRet);
    connect(actionContinue, &QAction::triggered, Core(), []() {
        // Switch between continue and suspend depending on the debugger's state
        if (Core()->isDebugTaskInProgress()) {
            Core()->suspendDebug();
        } else {
            Core()->continueDebug();
        }
    });

    connect(Config(), &Configuration::interfaceThemeChanged, this, &DebugActions::chooseThemeIcons);
    chooseThemeIcons();
}

void DebugActions::setButtonVisibleIfMainExists()
{
    // Use cmd because cmdRaw would not handle multiple commands concatenated
    int mainExists = Core()->cmd("f?sym.main; ??").toInt();
    // if main is not a flag we hide the continue until main button
    if (!mainExists) {
        actionContinueUntilMain->setVisible(false);
        continueUntilButton->setDefaultAction(actionContinueUntilCall);
    }
}

// unused
void DebugActions::showDebugWarning()
{
    if (!acceptedDebugWarning) {
        acceptedDebugWarning = true;
        QMessageBox msgBox;
        msgBox.setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
        msgBox.setText(
            tr("Debug is currently in beta.\n")
            + tr(
                "If you encounter any problems or have suggestions, please "
                "submit an issue to https://github.com/radareorg/iaito/issues"));
        msgBox.exec();
    }
}

void DebugActions::continueUntilMain()
{
    QString mainAddr = Core()->cmdRaw("?v sym.main");
    Core()->continueUntilDebug(mainAddr);
}

void DebugActions::continueUntilAddress()
{
    QString addr = QInputDialog::getText(
        main, tr("Continue until address"), tr("Address (e.g. 0x1234, sym.func):"));
    if (!addr.isEmpty()) {
        Core()->continueUntilDebug(addr);
    }
}

void DebugActions::continueUntilSignal()
{
    bool ok;
    int signal = QInputDialog::getInt(
        main, tr("Continue until signal"), tr("Signal number:"), 0, 0, 255, 1, &ok);
    if (ok) {
        Core()->continueUntilSignal(signal);
    }
}

#ifdef IAITO_ENABLE_DEBUGGER
void DebugActions::attachRemoteDebugger()
{
    QString stopAttachLabel = tr("Detach from process");
    // Hide unwanted buttons
    setAllActionsVisible(true);
    actionStart->setVisible(false);
    actionStartRemote->setVisible(false);
    actionStartEmul->setVisible(false);
    actionStop->setText(stopAttachLabel);
    actionStop->setIcon(detachIcon);
}

void DebugActions::onAttachedRemoteDebugger(bool successfully)
{
    if (!successfully) {
        QMessageBox msgBox;
        msgBox.setText(tr("Error connecting."));
        msgBox.exec();
        attachRemoteDialog();
    } else {
        QSettings settings;
        QStringList ips = settings.value("recentIpList").toStringList();
        ips.removeAll(remoteDialog->getUri());
        ips.prepend(remoteDialog->getUri());
        settings.setValue("recentIpList", ips);

        delete remoteDialog;
        remoteDialog = nullptr;
        attachRemoteDebugger();
    }
}

void DebugActions::attachRemoteDialog()
{
    // showDebugWarning();

    if (!remoteDialog) {
        remoteDialog = new RemoteDebugDialog(main);
    }
    QMessageBox msgBox;
    bool success = false;
    while (!success) {
        success = true;
        if (remoteDialog->exec()) {
            if (!remoteDialog->validate()) {
                success = false;
                continue;
            }

            Core()->attachRemote(remoteDialog->getUri());
        }
    }
}

void DebugActions::attachProcessDialog()
{
    // showDebugWarning();

    AttachProcDialog dialog(main);
    bool success = false;
    while (!success) {
        success = true;
        if (dialog.exec()) {
            int pid = dialog.getPID();
            if (pid >= 0) {
                attachProcess(pid);
            } else {
                success = false;
                QMessageBox msgBox;
                msgBox.setText(tr("Error attaching. No process selected!"));
                msgBox.exec();
            }
        }
    }
}

void DebugActions::attachProcess(int pid)
{
    QString stopAttachLabel = tr("Detach from process");
    // hide unwanted buttons
    setAllActionsVisible(true);
    actionStart->setVisible(false);
    actionStartRemote->setVisible(false);
    actionStartEmul->setVisible(false);
    actionStop->setText(stopAttachLabel);
    actionStop->setIcon(detachIcon);
    // attach
    Core()->attachDebug(pid);
}

void DebugActions::startDebug()
{
    // check if file is executable before starting debug
    QString filename = Core()->getFilePath();

    QFileInfo info(filename);
    if (!Core()->currentlyDebugging && !info.isExecutable()) {
        QMessageBox msgBox;
        msgBox.setText(tr("File '%1' does not have executable permissions.").arg(filename));
        msgBox.exec();
        return;
    }

    // showDebugWarning();

    NativeDebugDialog dialog(main);
    dialog.setArgs(Core()->getConfig("dbg.args"));
    QString args;
    if (dialog.exec()) {
        args = dialog.getArgs();
    } else {
        return;
    }

    // Update dbg.args with the new args
    Core()->setConfig("dbg.args", args);

    setAllActionsVisible(true);
    actionAttach->setVisible(false);
    actionStartRemote->setVisible(false);
    actionStartEmul->setVisible(false);
    actionStart->setText(restartDebugLabel);
    actionStart->setIcon(restartIcon);
    setButtonVisibleIfMainExists();

    Core()->startDebug();
}
#endif // IAITO_ENABLE_DEBUGGER

void DebugActions::setAllActionsVisible(bool visible)
{
    for (QAction *action : allActions) {
        action->setVisible(visible);
    }
}

void DebugActions::refreshStandardIcons()
{
    startEmulIcon = standardToolbarIcon(
        toolBar, QStyle::SP_MediaPlay, QStringLiteral(":/img/icons/play_light_emul.svg"));
    startAttachIcon = standardToolbarIcon(
        toolBar, QStyle::SP_MediaPlay, QStringLiteral(":/img/icons/play_light_attach.svg"));
    startRemoteIcon = standardToolbarIcon(
        toolBar, QStyle::SP_MediaPlay, QStringLiteral(":/img/icons/play_light_remote.svg"));
    stopIcon = standardToolbarIcon(
        toolBar, QStyle::SP_MediaStop, QStringLiteral(":/img/icons/media-stop_light.svg"));
    restartIcon = standardToolbarIcon(
        toolBar, QStyle::SP_BrowserReload, QStringLiteral(":/img/icons/spin_light.svg"));
    detachIcon = standardToolbarIcon(
        toolBar, QStyle::SP_DialogCloseButton, QStringLiteral(":/img/icons/detach_debugger.svg"));
    startDebugIcon = standardToolbarIcon(
        toolBar, QStyle::SP_MediaPlay, QStringLiteral(":/img/icons/play_light_debug.svg"));
    continueIcon = standardToolbarIcon(
        toolBar, QStyle::SP_MediaPlay, QStringLiteral(":/img/icons/media-skip-forward_light.svg"));
    suspendIcon = standardToolbarIcon(
        toolBar, QStyle::SP_MediaPause, QStringLiteral(":/img/icons/media-suspend_light.svg"));
}

/**
 * @brief When theme changed, change icons which have a special version for the
 * theme.
 */
void DebugActions::chooseThemeIcons()
{
    refreshStandardIcons();

#ifdef IAITO_ENABLE_DEBUGGER
    actionStart->setIcon(actionStart->text() == restartDebugLabel ? restartIcon : startDebugIcon);
    actionAttach->setIcon(startAttachIcon);
    actionStartRemote->setIcon(startRemoteIcon);
#endif
    actionStartEmul->setIcon(
        actionStartEmul->text() == tr("Restart emulation") ? restartIcon : startEmulIcon);
    actionStop->setIcon(actionStop->text() == tr("Detach from process") ? detachIcon : stopIcon);
    actionContinue->setIcon(Core()->isDebugTaskInProgress() ? suspendIcon : continueIcon);

    // List of QActions which have alternative icons in different themes
    const QList<QPair<void *, QString>> kSupportedIconsNames{
        {actionStep, QStringLiteral("step_into.svg")},
        {actionStepOver, QStringLiteral("step_over.svg")},
        {actionStepOut, QStringLiteral("step_out.svg")},
        {actionContinueUntilMain, QStringLiteral("continue_until_main.svg")},
        {actionContinueUntilCall, QStringLiteral("continue_until_call.svg")},
        {actionContinueUntilSyscall, QStringLiteral("continue_until_syscall.svg")},
    };

    // Set the correct icon for the QAction
    qhelpers::setThemeIcons(kSupportedIconsNames, [](void *obj, const QIcon &icon) {
        static_cast<QAction *>(obj)->setIcon(icon);
    });
}

void DebugActions::editRarunProfile()
{
    QString dbgProfile = Core()->getConfig("dbg.profile");
    if (dbgProfile.isEmpty()) {
        const QString configDir = QStandardPaths::writableLocation(
            QStandardPaths::AppConfigLocation);
        if (!configDir.isEmpty()) {
            QDir().mkpath(configDir);
            dbgProfile = QDir(configDir).filePath(QStringLiteral("profile.r2.txt"));
        } else {
            dbgProfile = QDir::home().filePath(QStringLiteral(".iaito-profile.r2.txt"));
        }
    }
    if (openTextEditDialogFromFile(dbgProfile)) {
        Core()->setConfig("dbg.profile", dbgProfile);
    } else {
        main->messageBoxWarning(tr("Error"), tr("Cannot save rarun2 profile."));
    }
}
