#include "SnapshotViewer.hpp"

#include <QFile>
#include <QVBoxLayout>
#include <algorithm>
#include <QSize>
#include <QDesktopServices>
#include <algorithm>
#include <map>

#include "src/Insight/InsightWorker/Tasks/WriteTargetMemory.hpp"
#include "src/Insight/InsightWorker/InsightWorker.hpp"

#include "src/Insight/UserInterfaces/InsightWindow/UiLoader.hpp"

#include "src/Insight/UserInterfaces/InsightWindow/Widgets/Label.hpp"
#include "src/Insight/UserInterfaces/InsightWindow/Widgets/ConfirmationDialog.hpp"

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

#include "src/Insight/InsightSignals.hpp"
#include "src/Logger/Logger.hpp"

namespace Widgets
{
    using Exceptions::Exception;

    SnapshotViewer::SnapshotViewer(
        MemorySnapshot& snapshot,
        const Targets::TargetAddressSpaceDescriptor& addressSpaceDescriptor,
        const Targets::TargetMemorySegmentDescriptor& memorySegmentDescriptor,
        const Targets::TargetState& targetState,
        QWidget* parent
    )
        : QWidget(parent)
        , snapshot(snapshot)
        , addressSpaceDescriptor(addressSpaceDescriptor)
        , memorySegmentDescriptor(memorySegmentDescriptor)
        , hexViewerData(snapshot.data)
    {
        this->setWindowFlag(Qt::Window);
        this->setObjectName("snapshot-viewer");
        this->setWindowTitle(this->snapshot.name + " (" + this->snapshot.id + ")");

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

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

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

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

        // Set ideal window size
        this->setFixedSize(1205, 850);
        this->setMinimumSize(700, 600);
        this->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);

        auto uiLoader = UiLoader{this};
        this->setStyleSheet(stylesheetFile.readAll());
        this->container = uiLoader.load(&windowUiFile, this);

        auto* containerLayout = this->container->findChild<QVBoxLayout*>();
        this->detailsContainer = this->container->findChild<QWidget*>("details-container");

        this->nameInput = this->detailsContainer->findChild<TextInput*>("name-input");
        this->descriptionInput = this->detailsContainer->findChild<PlainTextEdit*>("description-input");

        auto* detailsContainerLayout = this->detailsContainer->findChild<QHBoxLayout*>();
        detailsContainerLayout->setContentsMargins(0, 0, 0, 0);

        auto* attributesLayout = this->detailsContainer->findChild<QVBoxLayout*>("attributes-layout");
        attributesLayout->setContentsMargins(10, 10, 10, 0);

        auto* rightPanelLayout = this->detailsContainer->findChild<QVBoxLayout*>("right-panel-layout");
        rightPanelLayout->setContentsMargins(0, 0, 0, 0);

        if (!this->snapshot.excludedRegions.empty() || !this->snapshot.focusedRegions.empty()) {
            auto* memoryRegionsContainer = this->detailsContainer->findChild<QWidget*>("memory-regions-container");
            auto* memoryRegionsLayout = memoryRegionsContainer->findChild<QVBoxLayout*>();
            auto* noMemoryRegionsLabel = memoryRegionsContainer->findChild<Label*>("no-regions-label");

            std::transform(
                this->snapshot.focusedRegions.begin(),
                this->snapshot.focusedRegions.end(),
                std::back_inserter(this->memoryRegionItems),
                [] (const MemoryRegion& focusedRegion) {
                    return new MemoryRegionItem{focusedRegion};
                }
            );

            std::transform(
                this->snapshot.excludedRegions.begin(),
                this->snapshot.excludedRegions.end(),
                std::back_inserter(this->memoryRegionItems),
                [] (const MemoryRegion& excludedRegion) {
                    return new MemoryRegionItem{excludedRegion};
                }
            );

            this->memoryRegionListView = new ListView{
                ListItem::ListItemSetType{this->memoryRegionItems.begin(), this->memoryRegionItems.end()},
                this
            };
            this->memoryRegionListView->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);

            this->memoryRegionListScene = this->memoryRegionListView->listScene();
            this->memoryRegionListScene->margins = QMargins{0, 5, 0, 5};
            this->memoryRegionListScene->setSelectionLimit(2);

            noMemoryRegionsLabel->hide();
            memoryRegionsLayout->insertWidget(0, this->memoryRegionListView);
        }

        this->restoreBytesAction = new ContextMenuAction{
            "Restore Selection",
            [this] (const std::set<Targets::TargetMemoryAddress>&) {
                return this->memorySegmentDescriptor.debugModeAccess.writeable;
            },
            this
        };

        this->hexViewerWidget = new HexViewerWidget{
            this->addressSpaceDescriptor,
            this->memorySegmentDescriptor,
            targetState,
            this->hexViewerData,
            this->hexViewerWidgetSettings,
            this->snapshot.focusedRegions,
            this->snapshot.excludedRegions,
            this
        };

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

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

        auto* memoryCapacityLabel = this->bottomBar->findChild<Label*>("memory-capacity-label");
        auto* snapshotIdLabel = this->bottomBar->findChild<Label*>("id-label");
        auto* programCounterLabel = this->bottomBar->findChild<Label*>("program-counter-label");
        auto* dateLabel = this->bottomBar->findChild<Label*>("date-label");

        memoryCapacityLabel->setText(
            QLocale{QLocale::English}.toString(this->memorySegmentDescriptor.size()) + " bytes"
        );
        snapshotIdLabel->setText(this->snapshot.id);
        programCounterLabel->setText(
            "0x" + QString::number(this->snapshot.programCounter, 16).rightJustified(8, '0').toUpper()
        );
        dateLabel->setText(this->snapshot.createdDate.toString("dd/MM/yyyy hh:mm"));

        this->nameInput->setText(this->snapshot.name);
        this->descriptionInput->setPlainText(this->snapshot.description);

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

        QObject::connect(
            this->restoreBytesAction,
            &ContextMenuAction::invoked,
            this,
            [this] (const std::set<Targets::TargetMemoryAddress>& selectedByteItemAddresses) {
                this->restoreSelectedBytes(selectedByteItemAddresses, true);
            }
        );

        QObject::connect(this->hexViewerWidget, &HexViewerWidget::ready, this, &SnapshotViewer::onHexViewerReady);

        this->hexViewerWidget->init();
        this->move(this->parentWidget()->window()->geometry().center() - this->rect().center());
    }

    void SnapshotViewer::showEvent(QShowEvent* event) {
        QWidget::showEvent(event);
    }

    void SnapshotViewer::resizeEvent(QResizeEvent* event) {
        this->container->setFixedSize(this->size());
        QWidget::resizeEvent(event);
    }

    void SnapshotViewer::onHexViewerReady() {
        this->hexViewerWidget->addExternalContextMenuAction(this->restoreBytesAction);
        this->hexViewerWidget->setStackPointer(this->snapshot.stackPointer);
    }

    void SnapshotViewer::restoreSelectedBytes(
        std::set<Targets::TargetMemoryAddress> addresses,
        bool confirmationPromptEnabled
    ) {
        const auto excludedAddresses = this->snapshot.excludedAddresses();
        std::erase_if(addresses, [excludedAddresses] (const auto& address) {
            return excludedAddresses.contains(address);
        });

        if (addresses.empty()) {
            // The user has only selected bytes that are within an excluded region - nothing to do here
            return;
        }

        if (confirmationPromptEnabled) {
            auto* confirmationDialog = new ConfirmationDialog{
                "Restore selected bytes",
                "This operation will write " + QString::number(addresses.size()) + " byte(s) to the target's \""
                    + QString::fromStdString(this->memorySegmentDescriptor.name)
                    + "\" segment.<br/><br/>Are you sure you want to proceed?",
                "Proceed",
                std::nullopt,
                this
            };

            QObject::connect(
                confirmationDialog,
                &ConfirmationDialog::confirmed,
                this,
                [this, addresses] {
                    this->restoreSelectedBytes(addresses, false);
                }
            );

            confirmationDialog->show();
            return;
        }

        auto writeBlocks = std::vector<WriteTargetMemory::Block>{};

        auto blockStartAddress = *(addresses.begin());
        auto blockEndAddress = blockStartAddress;

        for (const auto& address : addresses) {
            if (address > (blockEndAddress + 1)) {
                // Commit the block
                const auto dataBeginOffset = blockStartAddress - this->memorySegmentDescriptor.addressRange.startAddress;
                const auto dataEndOffset = blockEndAddress - this->memorySegmentDescriptor.addressRange.startAddress + 1;

                writeBlocks.emplace_back(
                    blockStartAddress,
                    Targets::TargetMemoryBuffer{
                        this->snapshot.data.begin() + dataBeginOffset,
                        this->snapshot.data.begin() + dataEndOffset
                    }
                );

                blockStartAddress = address;
                blockEndAddress = address;
                continue;
            }

            blockEndAddress = address;
        }

        {
            const auto dataBeginOffset = blockStartAddress - this->memorySegmentDescriptor.addressRange.startAddress;
            const auto dataEndOffset = blockEndAddress - this->memorySegmentDescriptor.addressRange.startAddress + 1;

            writeBlocks.emplace_back(
                blockStartAddress,
                Targets::TargetMemoryBuffer{
                    this->snapshot.data.begin() + dataBeginOffset,
                    this->snapshot.data.begin() + dataEndOffset
                }
            );
        }

        const auto writeMemoryTask = QSharedPointer<WriteTargetMemory>{
            new WriteTargetMemory{this->addressSpaceDescriptor, this->memorySegmentDescriptor, std::move(writeBlocks)},
            &QObject::deleteLater
        };

        this->taskProgressIndicator->addTask(writeMemoryTask);
        InsightWorker::queueTask(writeMemoryTask);
    }
}
