#include "MainWindow.h"
#include "FactoryQueries.h"

#include <map>
#include <random>
#include <set>

#include <QApplication>
#include <QCloseEvent>
#include <QDir>
#include <QFile>
#include <QInputDialog>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QResizeEvent>
#include <QVBoxLayout>

#include "BlueprintLibrary.h"
#include "BlueprintSelectionDialog.h"
#include "BuildButtonBar.h"
#include "BuildingSystem.h"
#include "Command.h"
#include "CommandRequestedEvent.h"
#include "ConfigLoader.h"
#include "EventManager.h"
#include "GameWorldView.h"
#include "RecipeSelectionDialog.h"
#include "SchematicChoiceDialog.h"
#include "HeaderBar.h"
#include "SelectionPanel.h"
#include "ControlsPanel.h"
#include "ShipLayoutBlueprintSerializer.h"
#include "ShipLayoutDialog.h"
#include "BuildingIconCache.h"
#include "ItemIconCache.h"
#include "ModalPauseScope.h"
#include "Simulation.h"
#include "Tick.h"
#include "VisualsLoader.h"

MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
                       std::shared_ptr<ParsedReplay> replay, QWidget* parent)
    : QWidget(parent)
    , m_configDir(configDir)
    , m_visuals(VisualsLoader::load(configDir + "/visuals.toml"))
    , m_sim(sim)
    , m_replay(std::move(replay))
{
    setWindowTitle(tr("Dota Factory"));
    resize(1280, 768);

    // Item and building icons live alongside the config (siblings of the config dir),
    // read from disk at runtime the same way visuals.toml is (REQ-UI-ITEM-ICON,
    // REQ-UI-BUILD-ICON).
    const QString configDirPath = QString::fromStdString(m_configDir);
    m_itemIcons = std::make_unique<ItemIconCache>(
        QDir::cleanPath(configDirPath + "/../icons/items"));
    m_buildingIcons = std::make_unique<BuildingIconCache>(
        QDir::cleanPath(configDirPath + "/../icons/buildings"));

    m_headerBar = new HeaderBar(sim, &sim->getConfig(), m_itemIcons.get(), this);

    m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
                                        m_itemIcons.get(), m_replay.get(), this);

    // Floats over the game world at its bottom center, sized to its buttons
    // (REQ-UI-BUILD-BAR). Creation order is the stacking order for siblings, so
    // building it after the world view puts it above the world and its vignettes,
    // and before the dim overlay keeps modals dimming it too (REQ-UI-MODAL-DIM).
    // Its geometry comes from layoutPanels().
    m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(),
                                          m_buildingIcons.get(), m_itemIcons.get(),
                                          this);

    // The blueprints have no widget of their own: they are saved with Ctrl+C and picked
    // from a modal dialog (REQ-UI-BLUEPRINT-DIALOG), both driven from this window
    // because only it can pause the game and raise the dim overlay. Built after the
    // world view because loading blueprints.toml may put a message box on screen.
    m_blueprintLibrary = std::make_unique<BlueprintLibrary>(sim, &sim->getConfig(), this);
    // Two facts about blueprints decide what the world view offers the player
    // (REQ-UI-CONTROLS-CONTENT); the library is built after the view, so it is handed
    // over here rather than passed to the constructor.
    m_gameWorldView->setBlueprintLibrary(m_blueprintLibrary.get());

    // Floats over the game world at its right edge rather than occupying a column of
    // its own, and hides itself while nothing is selected (REQ-UI-SELECTION-PANEL). Like
    // the build button bar it is a sibling of the world view built after it, which is
    // what puts it above the world and its vignettes and below the dim overlay. It
    // brings its own chrome; its geometry comes from layoutPanels().
    m_selectionPanel = new SelectionPanel(sim, &sim->getConfig(), &m_visuals,
                                          m_itemIcons.get(), m_buildingIcons.get(),
                                          this);

    // Floats at the world view's opposite edge from the selection panel and reads the
    // world view for the player's current situation (REQ-UI-CONTROLS-PANEL). Built
    // after the view for the same stacking reason as the panels above it.
    m_controlsPanel = new ControlsPanel(m_gameWorldView, this);

    // Created last so it stacks above the other children; covers the whole window and
    // dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
    m_dimOverlay = new ModalDimOverlay(m_visuals.overlays.modalDim, this);

    m_gameWorldView->setFocus();

    connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
        if (newWidget && newWidget != m_gameWorldView && !QApplication::activeModalWidget())
        {
            m_gameWorldView->setFocus();
        }
    });

    // Load layout blueprints from disk. Missing file is silently ignored.
    const QString bpPath = QCoreApplication::applicationDirPath() + "/ship_layouts.toml";
    QFile bpFile(bpPath);
    if (bpFile.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        try
        {
            m_layoutBlueprints = ShipLayoutBlueprintSerializer::deserialize(
                bpFile.readAll().toStdString());
        }
        catch (const std::exception& e)
        {
            QMessageBox::critical(this, tr("Load Error"),
                tr("Failed to load ship_layouts.toml:\n%1").arg(e.what()));
            m_layoutBlueprints.clear();
        }
    }

    registerForEvents();
}

MainWindow::~MainWindow()
{
    unregisterForEvents();
}

void MainWindow::resizeEvent(QResizeEvent* event)
{
    QWidget::resizeEvent(event);
    layoutPanels();
}

void MainWindow::closeEvent(QCloseEvent* event)
{
    const QString path = QCoreApplication::applicationDirPath() + "/ship_layouts.toml";
    QFile file(path);
    if (file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        try
        {
            const std::string content =
                ShipLayoutBlueprintSerializer::serialize(m_layoutBlueprints);
            file.write(QByteArray::fromStdString(content));
        }
        catch (...) {}
    }
    QWidget::closeEvent(event);
}

void MainWindow::layoutPanels()
{
    const int totalW  = width();
    const int totalH  = height();
    const int headerH = m_headerBar->sizeHint().height();
    if (headerH <= 0) { return; }

    // Header bar and game world view span the full window width; the two floating
    // widgets below are the only things over the world (REQ-UI-HEADER,
    // REQ-UI-WORLD-SIZE).
    const QRect worldRect(0, headerH, totalW, totalH - headerH);
    m_headerBar->setGeometry(0, 0, totalW, headerH);
    m_gameWorldView->setGeometry(worldRect);
    // Sizes itself to its buttons and centers along the bottom of the world view
    // (REQ-UI-BUILD-BAR).
    m_buildButtonBar->anchorTo(worldRect);
    // The panel confines itself to what the bar leaves free, so the bar never has to
    // move for it (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
    m_selectionPanel->anchorTo(
        worldRect.adjusted(0, 0, 0, -m_buildButtonBar->getStripHeightPx()));
    // The opposite corner: bottom-left of the view, sharing the bottom edge with the
    // bar rather than clearing its whole strip, and rising only if the two would meet.
    // Anchored after the bar so the geometry it steps around is the current one
    // (REQ-UI-CONTROLS-PANEL).
    m_controlsPanel->anchorTo(worldRect, m_buildButtonBar->geometry());
    m_dimOverlay->setGeometry(0, 0, totalW, totalH);
}

void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
{
    ModalPauseScope pause(*m_gameWorldView);

    ModalDimScope dim(*m_dimOverlay);
    SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes, this);
    dialog.exec();

    std::shared_ptr<ApplySchematicChoiceCommand> command =
        std::make_shared<ApplySchematicChoiceCommand>();
    command->choiceIndex = dialog.getChosenIndex();
    EventManager::getInstance()->sendEventImmediately(
        std::make_shared<CommandRequestedEvent>(command));
}

void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*event*/)
{
    ModalPauseScope pause(*m_gameWorldView);

    ModalDimScope dim(*m_dimOverlay);
    QMessageBox box(this);
    box.setWindowTitle(tr("Paused"));
    QPushButton* continueBtn = box.addButton(tr("Continue"), QMessageBox::AcceptRole);
    QPushButton* restartBtn  = box.addButton(tr("Restart"),  QMessageBox::ResetRole);
    QPushButton* quitBtn     = box.addButton(tr("Quit"),     QMessageBox::DestructiveRole);
    box.setEscapeButton(continueBtn);
    box.exec();

    QAbstractButton* clicked = box.clickedButton();
    if (clicked == restartBtn)
    {
        std::optional<GameConfig> newConfig = reloadConfig();
        if (!newConfig.has_value())
        {
            return;
        }
        // Restart is a command boundary; the view resets when the drain applies
        // it (see GameWorldView::onFrame). A fresh random seed starts a new run.
        // resetForNewGame() sets the speed for the new run, so the pre-restart
        // speed is deliberately not restored here.
        pause.release();
        std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
        command->config = std::make_shared<GameConfig>(std::move(*newConfig));
        command->seed   = std::random_device{}();
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<CommandRequestedEvent>(command));
    }
    else if (clicked == quitBtn)
    {
        pause.release();
        close();
    }
}

std::optional<GameConfig> MainWindow::reloadConfig()
{
    // Config is reloaded from disk on every restart (REQ-CFG-RELOAD); a malformed
    // file must not leave the window half-updated, so the visuals are only applied
    // once both files have parsed.
    try
    {
        GameConfig    newConfig  = ConfigLoader::loadFromDirectory(m_configDir);
        VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
        m_visuals = std::move(newVisuals);
        m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
        return newConfig;
    }
    catch (const std::exception& e)
    {
        QMessageBox::critical(this, tr("Config Error"),
            tr("Failed to reload config:\n%1").arg(e.what()));
        return std::nullopt;
    }
}

void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
                                      const std::string& schematicId,
                                      const ShipLayoutConfig& currentLayout)
{
    ModalPauseScope pause(*m_gameWorldView);

    std::set<std::string> unlockedModuleIds;
    for (const ModuleDef& def : m_sim->getConfig().modules.modules)
    {
        if (m_sim->isModuleSchematicUnlocked(def.id))
        {
            unlockedModuleIds.insert(def.id);
        }
    }

    ModalDimScope dim(*m_dimOverlay);
    ShipLayoutDialog dialog(&m_sim->getConfig(), schematicId, currentLayout,
                            m_layoutBlueprints,
                            std::move(unlockedModuleIds),
                            m_gameWorldView->isDebugDrawEnabled(),
                            this);
    if (dialog.exec() == QDialog::Accepted && dialog.getResult().has_value())
    {
        std::shared_ptr<SetShipLayoutCommand> command =
            std::make_shared<SetShipLayoutCommand>();
        command->id     = shipyardId;
        command->layout = *dialog.getResult();
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<CommandRequestedEvent>(command));
    }
}

void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
{
    // A construction site has no Building yet; fall back to its site record so
    // the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
    const Building* b = findBuilding(m_sim->getFactoryState(), event->shipyardId);
    const ConstructionSite* s =
        b ? nullptr : findSite(m_sim->getFactoryState(), event->shipyardId);
    if (!b && !s)
    {
        return;
    }

    const std::string& schematicId = b ? b->recipeId : s->recipeId;
    const std::optional<ShipLayoutConfig>& layoutOpt =
        b ? b->shipLayout : s->shipLayout;

    ShipLayoutConfig currentLayout;
    if (layoutOpt.has_value())
    {
        currentLayout = *layoutOpt;
    }

    openShipLayoutDialog(event->shipyardId, schematicId, currentLayout);
}

void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event)
{
    ModalPauseScope pause(*m_gameWorldView);

    // A construction site has no Building yet; fall back to its site record so
    // the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG).
    const Building* b = findBuilding(m_sim->getFactoryState(), event->buildingId);
    const ConstructionSite* s =
        b ? nullptr : findSite(m_sim->getFactoryState(), event->buildingId);
    if (!b && !s)
    {
        return;
    }

    // Held across both the selection dialog and any auto-opened layout dialog so the
    // dim stays continuously visible through that sequence (REQ-UI-MODAL-DIM).
    ModalDimScope dim(*m_dimOverlay);

    const BuildingType type = b ? b->type : s->type;
    // Captured as a copy: a queued command may drain during the modal dialog's
    // event loop and reallocate the building vectors, so b/s must not be
    // dereferenced after dialog.exec() returns.
    const std::string oldSchematic = b ? b->recipeId : s->recipeId;
    const std::vector<RecipeSelectionOption> options =
        buildRecipeSelectionOptions(type, *m_sim, m_sim->getConfig());
    const QString title = (type == BuildingType::Shipyard)
        ? tr("Select Schematic")
        : tr("Select Recipe");

    bool autoOpenLayout = false;
    std::string chosenSchematic;
    RecipeSelectionDialog dialog(options, title, m_itemIcons.get(), this);
    if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
    {
        std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
        command->id       = event->buildingId;
        command->recipeId = *dialog.getChosenId();
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<CommandRequestedEvent>(command));

        // REQ-MOD-UI-AUTO-DIALOG: picking a new schematic for a shipyard opens the
        // layout configuration dialog immediately. Only on an actual change.
        if (type == BuildingType::Shipyard && *dialog.getChosenId() != oldSchematic)
        {
            autoOpenLayout = true;
            chosenSchematic = *dialog.getChosenId();
        }
    }

    // The SetRecipeCommand above is queued (drains on a later frame) and clears
    // the shipyard's layout, so open the dialog with the chosen schematic and an
    // empty layout rather than reading the not-yet-updated building state. Speed
    // is restored first so the helper snapshots the real speed to restore.
    pause.restore();
    if (autoOpenLayout)
    {
        openShipLayoutDialog(event->buildingId, chosenSchematic, ShipLayoutConfig{});
    }
}

void MainWindow::handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> /*event*/)
{
    // Ctrl+C has effect only when something player-placeable is selected; otherwise no
    // dialog opens at all (REQ-UI-BLUEPRINT-CREATE).
    if (!m_blueprintLibrary->getCanCaptureSelection()) { return; }

    // Both scopes are held across the naming dialog and the selection dialog it hands
    // over to, so the dim never blinks off and the simulation is not resumed in between
    // (REQ-UI-MODAL-DIM).
    ModalPauseScope pause(*m_gameWorldView);
    ModalDimScope   dim(*m_dimOverlay);

    bool ok = false;
    const QString name = QInputDialog::getText(
        this, tr("Create Blueprint"), tr("Blueprint name:"), QLineEdit::Normal,
        QString(), &ok);
    // Cancel, Escape, or an empty name: no blueprint, and no selection dialog.
    if (!ok || name.trimmed().isEmpty()) { return; }

    m_blueprintLibrary->saveSelectionAs(name.trimmed());
    showBlueprintSelectionDialog();
}

void MainWindow::handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> /*event*/)
{
    ModalPauseScope pause(*m_gameWorldView);
    ModalDimScope   dim(*m_dimOverlay);
    showBlueprintSelectionDialog();
}

void MainWindow::showBlueprintSelectionDialog()
{
    BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(), this);
    if (dialog.exec() == QDialog::Accepted && dialog.getChosenIndex().has_value())
    {
        // Entered after the dialog has closed, which is the order REQ-UI-BLUEPRINT-CARD
        // describes: clicking a card closes the dialog and enters placement mode.
        m_blueprintLibrary->beginPlacement(*dialog.getChosenIndex());
    }
}

void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
{
    const Tick tick = m_sim->getCurrentTick();
    const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
    const int minutes = totalSeconds / 60;
    const int seconds = totalSeconds % 60;

    ModalDimScope dim(*m_dimOverlay);
    QMessageBox box(this);
    box.setWindowTitle(tr("Game Over"));
    box.setText(tr("HQ destroyed!\nSurvival time: %1:%2")
        .arg(minutes, 2, 10, QChar('0'))
        .arg(seconds, 2, 10, QChar('0')));
    QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::AcceptRole);
    box.addButton(tr("Quit"), QMessageBox::RejectRole);
    box.exec();

    if (box.clickedButton() == restartBtn)
    {
        std::optional<GameConfig> newConfig = reloadConfig();
        if (!newConfig.has_value())
        {
            return;
        }
        // Restart is a command boundary; the view resets when the drain applies it.
        std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
        command->config = std::make_shared<GameConfig>(std::move(*newConfig));
        command->seed   = std::random_device{}();
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<CommandRequestedEvent>(command));
    }
    else
    {
        close();
    }
}

void MainWindow::handleEvent(std::shared_ptr<const WinEvent> /*event*/)
{
    const Tick tick = m_sim->getCurrentTick();
    const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
    const int minutes = totalSeconds / 60;
    const int seconds = totalSeconds % 60;

    ModalDimScope dim(*m_dimOverlay);
    QMessageBox box(this);
    box.setWindowTitle(tr("Won!"));
    box.setText(tr("You collected all artifacts!\nSurvival time: %1:%2")
        .arg(minutes, 2, 10, QChar('0'))
        .arg(seconds, 2, 10, QChar('0')));
    QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::AcceptRole);
    box.addButton(tr("Quit"), QMessageBox::RejectRole);
    box.exec();

    if (box.clickedButton() == restartBtn)
    {
        std::optional<GameConfig> newConfig = reloadConfig();
        if (!newConfig.has_value())
        {
            return;
        }
        // Restart is a command boundary; the view resets when the drain applies it.
        std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
        command->config = std::make_shared<GameConfig>(std::move(*newConfig));
        command->seed   = std::random_device{}();
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<CommandRequestedEvent>(command));
    }
    else
    {
        close();
    }
}
