#include "TargetMemoryInspectionPane.hpp"

#include <QVBoxLayout>
#include <QToolButton>
#include <QLocale>

#include "src/Insight/UserInterfaces/InsightWindow/UiLoader.hpp"
#include "src/Insight/InsightSignals.hpp"
#include "src/Insight/InsightWorker/InsightWorker.hpp"
#include "src/Insight/UserInterfaces/InsightWindow/Widgets/Label.hpp"

#include "src/Insight/InsightWorker/Tasks/ReadTargetMemory.hpp"
#include "src/Insight/InsightWorker/Tasks/ReadStackPointer.hpp"

#include "src/Services/PathService.hpp"
#include "src/Helpers/EnumToStringMappings.hpp"
#include "src/Exceptions/Exception.hpp"

namespace Widgets
{
    using namespace Exceptions;

    using Targets::TargetMemoryAddressRange;

    TargetMemoryInspectionPane::TargetMemoryInspectionPane(
        const Targets::TargetAddressSpaceDescriptor& addressSpaceDescriptor,
        const Targets::TargetMemorySegmentDescriptor& memorySegmentDescriptor,
        const Targets::TargetDescriptor& targetDescriptor,
        const Targets::TargetState& targetState,
        TargetMemoryInspectionPaneSettings& settings,
        PaneState& paneState,
        PanelWidget* parent
    )
        : PaneWidget(paneState, parent)
        , addressSpaceDescriptor(addressSpaceDescriptor)
        , memorySegmentDescriptor(memorySegmentDescriptor)
        , targetDescriptor(targetDescriptor)
        , targetState(targetState)
        , settings(settings)
    {
        this->setObjectName("target-memory-inspection-pane");

        const auto memoryName = QString::fromStdString(this->memorySegmentDescriptor.name);

        this->setWindowTitle("Memory Inspection - " + memoryName);
        this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);

        auto uiFile = QFile{
            QString::fromStdString(Services::PathService::compiledResourcesPath()
                + "/src/Insight/UserInterfaces/InsightWindow/Widgets/TargetMemoryInspectionPane/UiFiles/TargetMemoryInspectionPane.ui"
            )
        };

        auto stylesheetFile = QFile{
            QString::fromStdString(Services::PathService::compiledResourcesPath()
                + "/src/Insight/UserInterfaces/InsightWindow/Widgets/TargetMemoryInspectionPane/Stylesheets/TargetMemoryInspectionPane.qss"
            )
        };

        if (!uiFile.open(QFile::ReadOnly)) {
            throw Exception{"Failed to open MemoryInspectionPane UI file"};
        }

        if (!stylesheetFile.open(QFile::ReadOnly)) {
            throw Exception{"Failed to open MemoryInspectionPane stylesheet file"};
        }

        auto uiLoader = UiLoader{this};
        this->container = uiLoader.load(&uiFile, this);
        this->container->setStyleSheet(stylesheetFile.readAll());
        this->container->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);

        this->subContainerLayout = this->container->findChild<QHBoxLayout*>("container-sub-layout");

        this->titleBar = this->container->findChild<QWidget*>("title-bar");

        this->bottomBar = this->container->findChild<QWidget*>("bottom-bar");
        this->bottomBarLayout = this->bottomBar->findChild<QHBoxLayout*>();

        this->manageMemoryRegionsButton = this->container->findChild<SvgToolButton*>("manage-memory-regions-btn");
        this->manageMemorySnapshotsButton = this->container->findChild<QToolButton*>("manage-memory-snapshots-btn");

        this->refreshButton = this->container->findChild<SvgToolButton*>("refresh-memory-btn");
        this->refreshOnTargetStopAction = this->refreshButton->findChild<QAction*>("refresh-target-stopped");
        this->refreshOnActivationAction = this->refreshButton->findChild<QAction*>("refresh-activation");

        this->detachPaneButton = this->container->findChild<SvgToolButton*>("detach-pane-btn");
        this->attachPaneButton = this->container->findChild<SvgToolButton*>("attach-pane-btn");

        this->manageMemorySnapshotsButton->layout()->setContentsMargins(0, 0, 0, 0);

        this->staleDataLabelContainer = this->container->findChild<QWidget*>("stale-data-label");

        this->titleBar->layout()->setContentsMargins(7, 0, 7, 0);
        auto* titleLabel = this->titleBar->findChild<Label*>("title");
        titleLabel->setText(memoryName);

        auto* memoryCapacityLabel = this->container->findChild<Label*>("memory-capacity-label");
        memoryCapacityLabel->setText(
            QLocale{QLocale::English}.toString(this->memorySegmentDescriptor.size()) + " bytes"
        );

        // Quick sanity check to ensure the validity of persisted settings.
        this->sanitiseSettings();

        this->refreshButton->setDisabled(true);

        this->hexViewerWidget = new HexViewerWidget{
            this->addressSpaceDescriptor,
            this->memorySegmentDescriptor,
            this->targetState,
            this->data,
            this->settings.hexViewerWidgetSettings,
            this->settings.focusedMemoryRegions,
            this->settings.excludedMemoryRegions,
            this
        };
        this->hexViewerWidget->setDisabled(true);

        this->subContainerLayout->insertWidget(1, this->hexViewerWidget);

        this->hexViewerWidget->init();

        this->rightPanel = new PanelWidget{PanelWidgetType::RIGHT, this->settings.rightPanelState, this};
        this->rightPanel->setObjectName("right-panel");
        this->rightPanel->setMinimumResize(200);
        this->rightPanel->setHandleSize(6);
        this->subContainerLayout->insertWidget(2, this->rightPanel);

        this->snapshotManager = new SnapshotManager{
            this->addressSpaceDescriptor,
            this->memorySegmentDescriptor,
            this->targetDescriptor,
            this->targetState,
            this->data,
            this->staleData,
            this->settings.focusedMemoryRegions,
            this->settings.excludedMemoryRegions,
            this->stackPointer,
            this->settings.snapshotManagerState,
            this->rightPanel
        };
        this->rightPanel->layout()->addWidget(this->snapshotManager);

        this->setRefreshOnTargetStopEnabled(this->settings.refreshOnTargetStop);
        this->setRefreshOnActivationEnabled(this->settings.refreshOnActivation);

        this->taskProgressIndicator = new TaskProgressIndicator{this};
        this->bottomBarLayout->insertWidget(5, this->taskProgressIndicator);

        QObject::connect(
            this,
            &PaneWidget::paneActivated,
            this,
            &TargetMemoryInspectionPane::postActivate
        );

        QObject::connect(
            this,
            &PaneWidget::paneDeactivated,
            this,
            &TargetMemoryInspectionPane::postDeactivate
        );

        QObject::connect(
            this,
            &PaneWidget::paneAttached,
            this,
            &TargetMemoryInspectionPane::postAttach
        );

        QObject::connect(
            this,
            &PaneWidget::paneDetached,
            this,
            &TargetMemoryInspectionPane::postDetach
        );

        QObject::connect(
            this->manageMemoryRegionsButton,
            &QToolButton::clicked,
            this,
            &TargetMemoryInspectionPane::openMemoryRegionManagerWindow
        );

        QObject::connect(
            this->manageMemorySnapshotsButton,
            &QToolButton::clicked,
            this,
            &TargetMemoryInspectionPane::toggleMemorySnapshotManagerPane
        );

        QObject::connect(
            this->snapshotManager,
            &PaneWidget::paneActivated,
            this,
            [this] {
                this->manageMemorySnapshotsButton->setChecked(true);
            }
        );

        QObject::connect(
            this->snapshotManager,
            &PaneWidget::paneDeactivated,
            this,
            [this] {
                this->manageMemorySnapshotsButton->setChecked(false);
            }
        );

        QObject::connect(
            this->snapshotManager,
            &SnapshotManager::insightWorkerTaskCreated,
            this,
            &TargetMemoryInspectionPane::onSubtaskCreated
        );

        QObject::connect(
            this->snapshotManager,
            &SnapshotManager::snapshotRestored,
            this,
            &TargetMemoryInspectionPane::onSnapshotRestored
        );

        QObject::connect(
            this->refreshButton,
            &QToolButton::clicked,
            this,
            [this] {
                this->refreshMemoryValues();
            }
        );

        QObject::connect(
            this->refreshOnTargetStopAction,
            &QAction::triggered,
            this,
            [this] (bool checked) {
                this->setRefreshOnTargetStopEnabled(checked);
            }
        );

        QObject::connect(
            this->refreshOnActivationAction,
            &QAction::triggered,
            this,
            [this] (bool checked) {
                this->setRefreshOnActivationEnabled(checked);
            }
        );

        QObject::connect(
            this->detachPaneButton,
            &QToolButton::clicked,
            this,
            &TargetMemoryInspectionPane::detach
        );

        QObject::connect(
            this->attachPaneButton,
            &QToolButton::clicked,
            this,
            &TargetMemoryInspectionPane::attach
        );

        auto* insightSignals = InsightSignals::instance();

        QObject::connect(
            insightSignals,
            &InsightSignals::targetStateUpdated,
            this,
            &TargetMemoryInspectionPane::onTargetStateChanged
        );

        QObject::connect(
            insightSignals,
            &InsightSignals::targetReset,
            this,
            &TargetMemoryInspectionPane::onTargetReset
        );

        QObject::connect(
            insightSignals,
            &InsightSignals::programmingModeEnabled,
            this,
            &TargetMemoryInspectionPane::onProgrammingModeEnabled
        );

        QObject::connect(
            insightSignals,
            &InsightSignals::programmingModeDisabled,
            this,
            &TargetMemoryInspectionPane::onProgrammingModeDisabled
        );

        QObject::connect(
            insightSignals,
            &InsightSignals::targetMemoryWritten,
            this,
            &TargetMemoryInspectionPane::onTargetMemoryWritten
        );

        // Restore the state
        if (this->state.attached) {
            this->attach();

        } else {
            this->detach();
        }

        if (this->state.activated) {
            this->activate();

        } else {
            this->deactivate();
        }

        this->snapshotManager->deactivate();
    }

    void TargetMemoryInspectionPane::refreshMemoryValues(std::optional<std::function<void(void)>> callback) {
        this->refreshButton->setDisabled(true);
        this->refreshButton->startSpin();

        auto excludedAddressRanges = std::set<Targets::TargetMemoryAddressRange>{};
        std::transform(
            this->settings.excludedMemoryRegions.begin(),
            this->settings.excludedMemoryRegions.end(),
            std::inserter(excludedAddressRanges, excludedAddressRanges.begin()),
            [this] (const ExcludedMemoryRegion& excludedRegion) {
                return excludedRegion.addressRange;
            }
        );

        const auto readMemoryTask = QSharedPointer<ReadTargetMemory>{
            new ReadTargetMemory{
                this->addressSpaceDescriptor,
                this->memorySegmentDescriptor,
                this->memorySegmentDescriptor.addressRange.startAddress,
                this->memorySegmentDescriptor.size(),
                excludedAddressRanges
            },
            &QObject::deleteLater
        };

        QObject::connect(
            readMemoryTask.get(),
            &ReadTargetMemory::targetMemoryRead,
            this,
            [this, callback] (const Targets::TargetMemoryBuffer& data) {
                this->onMemoryRead(data);

                // Refresh the stack pointer if this is RAM.
                if (this->memorySegmentDescriptor.type == Targets::TargetMemorySegmentType::RAM) {
                    const auto readStackPointerTask = QSharedPointer<ReadStackPointer>{
                        new ReadStackPointer{},
                        &QObject::deleteLater
                    };

                    QObject::connect(
                        readStackPointerTask.get(),
                        &ReadStackPointer::stackPointerRead,
                        this,
                        [this] (Targets::TargetStackPointer stackPointer) {
                            this->stackPointer = stackPointer;
                            this->hexViewerWidget->setStackPointer(stackPointer);
                        }
                    );

                    QObject::connect(
                        readStackPointerTask.get(),
                        &InsightWorkerTask::finished,
                        this,
                        [this] {
                            this->snapshotManager->onCurrentDataChanged();

                            this->refreshButton->stopSpin();

                            if (this->targetState.executionState == Targets::TargetExecutionState::STOPPED) {
                                this->refreshButton->setDisabled(false);
                            }
                        }
                    );

                    if (callback.has_value()) {
                        QObject::connect(
                            readStackPointerTask.get(),
                            &InsightWorkerTask::completed,
                            this,
                            callback.value()
                        );
                    }

                    this->taskProgressIndicator->addTask(readStackPointerTask);
                    InsightWorker::queueTask(readStackPointerTask);

                } else {
                    this->snapshotManager->onCurrentDataChanged();
                }
            }
        );

        QObject::connect(
            readMemoryTask.get(),
            &InsightWorkerTask::finished,
            this,
            [this, taskId = readMemoryTask->id] {
                if (this->activeRefreshTask.has_value() && this->activeRefreshTask->get()->id == taskId) {
                    this->activeRefreshTask.reset();
                }
            }
        );

        // If we're refreshing RAM, the UI should only be updated once we've retrieved the current stack pointer.
        if (this->memorySegmentDescriptor.type != Targets::TargetMemorySegmentType::RAM) {
            QObject::connect(
                readMemoryTask.get(),
                &InsightWorkerTask::finished,
                this,
                [this] {
                    this->refreshButton->stopSpin();

                    if (this->targetState.executionState == Targets::TargetExecutionState::STOPPED) {
                        this->refreshButton->setDisabled(false);
                    }
                }
            );

            if (callback.has_value()) {
                QObject::connect(
                    readMemoryTask.get(),
                    &InsightWorkerTask::completed,
                    this,
                    callback.value()
                );
            }

        } else {
            QObject::connect(
                readMemoryTask.get(),
                &InsightWorkerTask::failed,
                this,
                [this] {
                    this->refreshButton->stopSpin();

                    if (this->targetState.executionState == Targets::TargetExecutionState::STOPPED) {
                        this->refreshButton->setDisabled(false);
                    }
                }
            );
        }

        this->activeRefreshTask = readMemoryTask;
        this->taskProgressIndicator->addTask(readMemoryTask);
        InsightWorker::queueTask(readMemoryTask);
    }

    void TargetMemoryInspectionPane::resizeEvent(QResizeEvent* event) {
        const auto size = this->size();
        this->container->setFixedSize(size.width(), size.height());

        this->rightPanel->setMaximumResize(static_cast<int>(size.width() * 0.4));

        PaneWidget::resizeEvent(event);
    }

    void TargetMemoryInspectionPane::keyPressEvent(QKeyEvent* event) {
        if ((event->modifiers() & Qt::ControlModifier) != 0 && event->key() == Qt::Key::Key_R) {
            this->refreshButton->click();
            event->accept();
        }
    }

    void TargetMemoryInspectionPane::postActivate() {
        if (this->targetState.executionState == Targets::TargetExecutionState::STOPPED) {
            if (
                !this->activeRefreshTask.has_value()
                && (this->settings.refreshOnActivation || !this->data.has_value())
            ) {
                this->refreshMemoryValues([this] {
                    this->hexViewerWidget->setDisabled(false);
                });

            } else if (this->data.has_value()) {
                this->hexViewerWidget->setDisabled(false);
                this->refreshButton->setDisabled(false);
            }
        }
    }

    void TargetMemoryInspectionPane::postDeactivate() {

    }

    void TargetMemoryInspectionPane::postAttach() {
        this->attachPaneButton->hide();
        this->detachPaneButton->show();
    }

    void TargetMemoryInspectionPane::postDetach() {
        this->detachPaneButton->hide();
        this->attachPaneButton->show();
    }

    void TargetMemoryInspectionPane::sanitiseSettings() {
        // Remove any invalid memory regions. It's very unlikely that there will be any, but not impossible.
        auto processedFocusedMemoryRegions = std::vector<FocusedMemoryRegion>{};
        auto processedExcludedMemoryRegions = std::vector<ExcludedMemoryRegion>{};

        const auto regionIntersects = [
            &processedFocusedMemoryRegions,
            &processedExcludedMemoryRegions
        ] (const MemoryRegion& region) {
            for (const auto& processedFocusedRegion : processedFocusedMemoryRegions) {
                if (processedFocusedRegion.intersectsWith(region)) {
                    return true;
                }
            }

            for (const auto& processedExcludedRegion : processedExcludedMemoryRegions) {
                if (processedExcludedRegion.intersectsWith(region)) {
                    return true;
                }
            }

            return false;
        };

        for (const auto& focusedRegion : this->settings.focusedMemoryRegions) {
            if (
                !this->memorySegmentDescriptor.addressRange.contains(focusedRegion.addressRange)
                || regionIntersects(focusedRegion)
            ) {
                continue;
            }

            processedFocusedMemoryRegions.emplace_back(focusedRegion);
        }

        for (const auto& excludedRegion : this->settings.excludedMemoryRegions) {
            if (
                !this->memorySegmentDescriptor.addressRange.contains(excludedRegion.addressRange)
                || regionIntersects(excludedRegion)
            ) {
                continue;
            }

            processedExcludedMemoryRegions.emplace_back(excludedRegion);
        }

        this->settings.focusedMemoryRegions = std::move(processedFocusedMemoryRegions);
        this->settings.excludedMemoryRegions = std::move(processedExcludedMemoryRegions);
    }

    void TargetMemoryInspectionPane::onTargetStateChanged(
        Targets::TargetState newState,
        Targets::TargetState previousState
    ) {
        if (newState.executionState == previousState.executionState) {
            return;
        }

        using Targets::TargetExecutionState;

        if (newState.executionState == TargetExecutionState::STOPPED) {
            if (this->state.activated && (this->settings.refreshOnTargetStop || !this->data.has_value())) {
                this->refreshMemoryValues([this] {
                    this->hexViewerWidget->setDisabled(false);
                });

            } else if (this->data.has_value()) {
                this->refreshButton->setDisabled(false);
                this->hexViewerWidget->setDisabled(false);
            }
        }

        if (newState.executionState == TargetExecutionState::RUNNING) {
            this->hexViewerWidget->setDisabled(true);
            this->refreshButton->setDisabled(true);

            if (this->data.has_value()) {
                this->setStaleData(true);
            }
        }

        this->snapshotManager->createSnapshotWindow->refreshForm();
    }

    void TargetMemoryInspectionPane::setRefreshOnTargetStopEnabled(bool enabled) {
        this->refreshOnTargetStopAction->setChecked(enabled);
        this->settings.refreshOnTargetStop = enabled;
    }

    void TargetMemoryInspectionPane::setRefreshOnActivationEnabled(bool enabled) {
        this->refreshOnActivationAction->setChecked(enabled);
        this->settings.refreshOnActivation = enabled;
    }

    void TargetMemoryInspectionPane::onMemoryRead(const Targets::TargetMemoryBuffer& data) {
        assert(data.size() == this->memorySegmentDescriptor.size());

        this->data = data;
        this->hexViewerWidget->updateValues();
        this->setStaleData(false);
    }

    void TargetMemoryInspectionPane::openMemoryRegionManagerWindow() {
        if (this->memoryRegionManagerWindow == nullptr) {
            this->memoryRegionManagerWindow = new MemoryRegionManagerWindow{
                this->addressSpaceDescriptor,
                this->memorySegmentDescriptor,
                this->settings.focusedMemoryRegions,
                this->settings.excludedMemoryRegions,
                this
            };

            QObject::connect(
                this->memoryRegionManagerWindow,
                &MemoryRegionManagerWindow::changesApplied,
                this,
                &TargetMemoryInspectionPane::onMemoryRegionsChange
            );
        }

        if (!this->memoryRegionManagerWindow->isVisible()) {
            this->memoryRegionManagerWindow->refreshRegions();
            this->memoryRegionManagerWindow->show();

        } else {
            this->memoryRegionManagerWindow->activateWindow();
        }
    }

    void TargetMemoryInspectionPane::toggleMemorySnapshotManagerPane() {
        if (!this->snapshotManager->state.activated) {
            this->snapshotManager->activate();
            return;
        }

        this->snapshotManager->deactivate();
    }

    void TargetMemoryInspectionPane::onMemoryRegionsChange() {
        this->hexViewerWidget->refreshRegions();
    }

    void TargetMemoryInspectionPane::onTargetReset() {
        if (this->data.has_value()) {
            this->setStaleData(true);
        }
    }

    void TargetMemoryInspectionPane::onProgrammingModeEnabled() {
        this->hexViewerWidget->setDisabled(true);
        this->refreshButton->setDisabled(true);

        if (this->data.has_value()) {
            this->setStaleData(true);
        }
    }

    void TargetMemoryInspectionPane::onProgrammingModeDisabled() {
        const auto disabled = this->targetState.executionState != Targets::TargetExecutionState::STOPPED
            || !this->data.has_value();
        this->hexViewerWidget->setDisabled(disabled);
        this->refreshButton->setDisabled(disabled);
    }

    void TargetMemoryInspectionPane::onTargetMemoryWritten(
        const Targets::TargetAddressSpaceDescriptor& addressSpaceDescriptor,
        const Targets::TargetMemorySegmentDescriptor& memorySegmentDescriptor,
        Targets::TargetMemoryAddressRange addressRange
    ) {
        if (memorySegmentDescriptor == this->memorySegmentDescriptor && this->data.has_value()) {
            this->setStaleData(true);
            this->snapshotManager->createSnapshotWindow->refreshForm();
        }
    }

    void TargetMemoryInspectionPane::onSubtaskCreated(const QSharedPointer<InsightWorkerTask>& task) {
        this->taskProgressIndicator->addTask(task);
    }

    void TargetMemoryInspectionPane::onSnapshotRestored(const QString& snapshotId) {
        this->refreshButton->click();
    }

    void TargetMemoryInspectionPane::setStaleData(bool staleData) {
        this->staleData = staleData;
        this->staleDataLabelContainer->setVisible(this->staleData);
    }
}
