#include "ShipLayoutDialog.h"
#include "ShipStatsPanel.h"

#include <cmath>
#include <functional>

#include "DisplayName.h"
#include "ModalLayer.h"
#include "NameInputDialog.h"
#include "OptionButton.h"
#include "ProductionRules.h"
#include "RecipeLineRow.h"
#include "SectionBox.h"
#include "TooltipTrigger.h"

#include <QGridLayout>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
#include <QElapsedTimer>
#include <QScrollArea>
#include <QSignalMapper>
#include <QTimer>
#include <QVBoxLayout>

namespace
{

std::vector<RecipeLineRow::Amount> toAmounts(
    const std::vector<RecipeIngredient>& ingredients)
{
    std::vector<RecipeLineRow::Amount> amounts;
    amounts.reserve(ingredients.size());
    for (const RecipeIngredient& ingredient : ingredients)
    {
        amounts.push_back(RecipeLineRow::Amount{ ingredient.item, ingredient.amount });
    }
    return amounts;
}

const int kCellSize = 32;

// Empty ship tiles pulse between this grey and white at 1 Hz while a module is
// selected, to draw the player's attention to where modules can be placed.
const double kTwoPi = 6.283185307179586;
const int kPulseIntervalMs = 33;
const int kEmptyCellGrey = 240;
const int kPulseAmplitude = 15;

std::vector<std::string> rotateMaskCW(const std::vector<std::string>& grid)
{
    if (grid.empty())
    {
        return {};
    }
    const int srcH = static_cast<int>(grid.size());
    int srcW = 0;
    for (const std::string& row : grid)
    {
        const int w = static_cast<int>(row.size());
        if (w > srcW)
        {
            srcW = w;
        }
    }
    const int dstW = srcH;
    const int dstH = srcW;
    std::vector<std::string> dst(dstH, std::string(dstW, 'X'));
    for (int row = 0; row < srcH; ++row)
    {
        for (int col = 0; col < srcW; ++col)
        {
            const char ch = (col < static_cast<int>(grid[row].size()))
                                ? grid[row][col]
                                : 'X';
            dst[col][srcH - 1 - row] = ch;
        }
    }
    return dst;
}

}  // namespace


// ---------------------------------------------------------------------------
// Grid rendering widget (nested inside dialog)
// ---------------------------------------------------------------------------

class LayoutGridWidget : public QWidget
{
public:
    LayoutGridWidget(ShipLayoutDialog* dialog, QWidget* parent = nullptr)
        : QWidget(parent)
        , m_dialog(dialog)
    {
        setMouseTracking(true);
        m_pulseClock.start();
        m_pulseTimer = new QTimer(this);
        m_pulseTimer->setInterval(kPulseIntervalMs);
        connect(m_pulseTimer, &QTimer::timeout, this, [this]() { update(); });
    }

    void setGridData(const std::vector<std::vector<ShipLayoutDialog::CellInfo>>* grid,
                     int rows, int cols,
                     const std::vector<PlacedModule>* placed,
                     const GameConfig* config)
    {
        m_grid = grid;
        m_rows = rows;
        m_cols = cols;
        m_placed = placed;
        m_config = config;
        setFixedSize(cols * kCellSize + 1, rows * kCellSize + 1);
    }

    void setGhostData(std::optional<int> moduleIndex, Rotation rotation)
    {
        m_ghostModuleIdx = moduleIndex;
        m_ghostRotation = rotation;
        // Only animate the empty-tile pulse while a module is selected.
        if (moduleIndex.has_value())
        {
            if (!m_pulseTimer->isActive())
            {
                m_pulseClock.restart();
                m_pulseTimer->start();
            }
        }
        else
        {
            m_pulseTimer->stop();
        }
    }

protected:
    void paintEvent(QPaintEvent* /*event*/) override
    {
        if (!m_grid || m_rows == 0 || m_cols == 0)
        {
            return;
        }

        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing, false);

        // While a module is selected, pulse empty tiles at 1 Hz around the base
        // grey (kEmptyCellGrey +/- kPulseAmplitude). All empty tiles share the
        // same phase, so compute it once.
        QColor emptyCellColor(kEmptyCellGrey, kEmptyCellGrey, kEmptyCellGrey);
        if (m_ghostModuleIdx.has_value())
        {
            const double seconds = m_pulseClock.elapsed() / 1000.0;
            const double oscillation = std::sin(seconds * kTwoPi);
            const int channel = kEmptyCellGrey
                + static_cast<int>(std::lround(oscillation * kPulseAmplitude));
            emptyCellColor = QColor(channel, channel, channel);
        }

        for (int r = 0; r < m_rows; ++r)
        {
            for (int c = 0; c < m_cols; ++c)
            {
                const QRect cellRect(c * kCellSize, r * kCellSize, kCellSize, kCellSize);
                const ShipLayoutDialog::CellInfo& cell = (*m_grid)[r][c];

                if (!cell.buildable)
                {
                    painter.fillRect(cellRect, QColor(30, 30, 30));
                }
                else if (cell.moduleIndex.has_value())
                {
                    const PlacedModule& pm = (*m_placed)[*cell.moduleIndex];
                    const ModuleDef* def = findModule(pm.moduleId);
                    QColor color(Qt::gray);
                    QString glyph;
                    if (def)
                    {
                        color = QColor(QString::fromStdString(def->fillColor));
                        glyph = QString::fromStdString(def->glyph);
                    }
                    painter.fillRect(cellRect, color);
                    if (!glyph.isEmpty())
                    {
                        painter.setPen(Qt::white);
                        painter.drawText(cellRect, Qt::AlignCenter, glyph);
                    }
                }
                else
                {
                    painter.fillRect(cellRect, emptyCellColor);
                }

                painter.setPen(QColor(100, 100, 100));
                painter.drawRect(cellRect);
            }
        }

        // Draw ghost
        if (m_ghostModuleIdx.has_value() && m_hoverCell.x() >= 0 && m_config)
        {
            const ModuleDef& def = m_config->modules.modules[*m_ghostModuleIdx];
            const std::vector<std::string> mask = rotateMask(def.surfaceMask, m_ghostRotation);
            QColor ghostColor(QString::fromStdString(def.fillColor));
            ghostColor.setAlpha(100);

            for (int mr = 0; mr < static_cast<int>(mask.size()); ++mr)
            {
                for (int mc = 0; mc < static_cast<int>(mask[mr].size()); ++mc)
                {
                    if (mask[mr][mc] != 'O')
                    {
                        continue;
                    }
                    const int gr = m_hoverCell.y() + mr;
                    const int gc = m_hoverCell.x() + mc;
                    if (gr >= 0 && gr < m_rows && gc >= 0 && gc < m_cols)
                    {
                        const QRect cellRect(gc * kCellSize, gr * kCellSize,
                                             kCellSize, kCellSize);
                        painter.fillRect(cellRect, ghostColor);
                    }
                }
            }
        }
    }

    void mouseMoveEvent(QMouseEvent* event) override
    {
        const QPoint pos = event->pos();
        const QPoint cell(pos.x() / kCellSize, pos.y() / kCellSize);
        if (cell != m_hoverCell)
        {
            m_hoverCell = cell;
            update();
        }
    }

    void mousePressEvent(QMouseEvent* event) override
    {
        if (event->button() != Qt::LeftButton)
        {
            return;
        }
        const QPoint pos = event->pos();
        const QPoint cell(pos.x() / kCellSize, pos.y() / kCellSize);
        emit m_dialog->gridCellClicked(cell);
    }

    void leaveEvent(QEvent* /*event*/) override
    {
        m_hoverCell = QPoint(-1, -1);
        update();
    }

private:
    const ModuleDef* findModule(const std::string& id) const
    {
        if (!m_config)
        {
            return nullptr;
        }
        return m_config->modules.findModuleDef(id);
    }

    std::vector<std::string> rotateMask(const std::vector<std::string>& mask,
                                        Rotation rotation) const
    {
        int steps = 0;
        switch (rotation)
        {
            case Rotation::East:  steps = 0; break;
            case Rotation::South: steps = 1; break;
            case Rotation::West:  steps = 2; break;
            case Rotation::North: steps = 3; break;
        }
        std::vector<std::string> result = mask;
        for (int i = 0; i < steps; ++i)
        {
            result = rotateMaskCW(result);
        }
        return result;
    }

    ShipLayoutDialog* m_dialog;
    const std::vector<std::vector<ShipLayoutDialog::CellInfo>>* m_grid = nullptr;
    int m_rows = 0;
    int m_cols = 0;
    const std::vector<PlacedModule>* m_placed = nullptr;
    const GameConfig* m_config = nullptr;
    std::optional<int> m_ghostModuleIdx;
    Rotation m_ghostRotation = Rotation::East;
    QPoint m_hoverCell = QPoint(-1, -1);
    QTimer* m_pulseTimer = nullptr;
    QElapsedTimer m_pulseClock;
};


// ---------------------------------------------------------------------------
// Blueprint panel (third column of the layout dialog)
// ---------------------------------------------------------------------------

class ShipLayoutBlueprintPanel : public QWidget
{
public:
    ShipLayoutBlueprintPanel(
        std::vector<ShipLayoutBlueprint>& allBlueprints,
        const std::string& shipType,
        std::function<std::vector<PlacedModule>()> getModules,
        std::function<void(const std::vector<PlacedModule>&)> loadModules,
        QWidget* parent = nullptr)
        : QWidget(parent)
        , m_allBlueprints(allBlueprints)
        , m_shipType(shipType)
        , m_getModules(std::move(getModules))
        , m_loadModules(std::move(loadModules))
    {
        QVBoxLayout* layout = new QVBoxLayout(this);
        layout->setContentsMargins(4, 4, 4, 4);
        layout->setSpacing(4);

        QPushButton* createBtn = new QPushButton(tr("Create Blueprint"), this);
        createBtn->setFixedHeight(36);
        layout->addWidget(createBtn);

        m_scrollArea = new QScrollArea(this);
        m_scrollArea->setWidgetResizable(true);
        m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

        m_scrollContainer = new QWidget(m_scrollArea);
        m_listLayout = new QVBoxLayout(m_scrollContainer);
        m_listLayout->setContentsMargins(0, 0, 0, 0);
        m_listLayout->setSpacing(2);
        m_listLayout->addStretch();
        m_scrollArea->setWidget(m_scrollContainer);
        layout->addWidget(m_scrollArea, 1);

        connect(createBtn, &QPushButton::clicked, this, [this]() {
            // Opened on the same layer as the dialog this panel sits in, so it stacks
            // over it on the one dim (REQ-MOD-UI-BLUEPRINT-CREATE, REQ-UI-MODAL-DIM).
            ModalLayer* layer = ModalLayer::findFor(*this);
            if (layer == nullptr) { return; }

            NameInputDialog nameDialog(tr("Create Blueprint"), tr("Blueprint name:"));
            if (layer->execute(nameDialog) != QDialog::Accepted) { return; }
            const QString name = nameDialog.getName();
            if (name.isEmpty()) { return; }

            ShipLayoutBlueprint bp;
            bp.name     = name;
            bp.shipType = m_shipType;
            bp.modules  = m_getModules();
            m_allBlueprints.push_back(std::move(bp));
            rebuildList();
        });

        rebuildList();
    }

private:
    void rebuildList()
    {
        // Remove all items except the trailing stretch.
        while (m_listLayout->count() > 1)
        {
            QLayoutItem* item = m_listLayout->takeAt(0);
            if (item->widget()) { delete item->widget(); }
            delete item;
        }

        for (std::size_t i = 0; i < m_allBlueprints.size(); ++i)
        {
            const ShipLayoutBlueprint& bp = m_allBlueprints[i];
            if (bp.shipType != m_shipType) { continue; }

            QWidget* row = new QWidget(m_scrollContainer);
            QHBoxLayout* rowLayout = new QHBoxLayout(row);
            rowLayout->setContentsMargins(0, 0, 0, 0);
            rowLayout->setSpacing(2);

            QPushButton* nameBtn = new QPushButton(bp.name, row);
            nameBtn->setFixedHeight(36);

            QPushButton* delBtn = new QPushButton("\xc3\x97", row);
            delBtn->setFixedWidth(28);
            delBtn->setFixedHeight(36);

            rowLayout->addWidget(nameBtn, 1);
            rowLayout->addWidget(delBtn, 0);
            m_listLayout->insertWidget(m_listLayout->count() - 1, row);

            const std::vector<PlacedModule> modules = bp.modules;
            connect(nameBtn, &QPushButton::clicked, this, [this, modules]() {
                m_loadModules(modules);
            });

            connect(delBtn, &QPushButton::clicked, this, [this, i]() {
                m_allBlueprints.erase(m_allBlueprints.begin()
                                      + static_cast<std::ptrdiff_t>(i));
                rebuildList();
            });
        }
    }

    std::vector<ShipLayoutBlueprint>&                    m_allBlueprints;
    std::string                                          m_shipType;
    std::function<std::vector<PlacedModule>()>           m_getModules;
    std::function<void(const std::vector<PlacedModule>&)> m_loadModules;
    QScrollArea* m_scrollArea     = nullptr;
    QWidget*     m_scrollContainer = nullptr;
    QVBoxLayout* m_listLayout     = nullptr;
};


// ---------------------------------------------------------------------------
// ShipLayoutDialog implementation
// ---------------------------------------------------------------------------

ShipLayoutDialog::ShipLayoutDialog(const std::string& shipId,
                                   const ShipLayoutConfig& currentLayout,
                                   std::vector<ShipLayoutBlueprint>& allBlueprints,
                                   std::set<std::string> unlockedModuleIds,
                                   bool debugDraw,
                                   const ItemTooltipContext& context,
                                   QWidget* parent)
    : ModalDialog(parent)
    , m_context(context)
    , m_config(context.config)
    , m_itemIcons(context.itemIcons)
    , m_shipId(shipId)
    , m_unlockedModuleIds(std::move(unlockedModuleIds))
    , m_rows(0)
    , m_cols(0)
    , m_placedModules(currentLayout.placedModules)
    , m_currentRotation(Rotation::East)
    , m_removeButton(nullptr)
    , m_gridWidget(nullptr)
    , m_statsPanel(nullptr)
    , m_debugDraw(debugDraw)
{
    // Find the ship's layout grid.
    const ShipDef* shipDef = m_config->ships.findShipDef(shipId);
    if (shipDef)
    {
        m_shipLayout = shipDef->layout;
    }

    m_rows = static_cast<int>(m_shipLayout.size());
    m_cols = 0;
    for (const std::string& row : m_shipLayout)
    {
        const int w = static_cast<int>(row.size());
        if (w > m_cols)
        {
            m_cols = w;
        }
    }

    // Initialize grid.
    m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, std::nullopt}));
    for (int r = 0; r < m_rows; ++r)
    {
        for (int c = 0; c < static_cast<int>(m_shipLayout[r].size()); ++c)
        {
            if (m_shipLayout[r][c] == 'O')
            {
                m_grid[r][c].buildable = true;
            }
        }
    }
    rebuildOccupancy();

    // --- UI layout ---
    QVBoxLayout* outerLayout = new QVBoxLayout(this);

    // The dialog's own title line; it carries no close button, having Confirm and Cancel
    // of its own at the bottom (REQ-UI-MODAL-CHROME, REQ-MOD-UI-DIALOG).
    addHeader(outerLayout, tr("Configure Ship Layout"), false);

    // Top: grid widget.
    LayoutGridWidget* gridW = new LayoutGridWidget(this, this);
    gridW->setGridData(&m_grid, m_rows, m_cols, &m_placedModules, m_config);
    gridW->setGhostData(m_activeModuleIndex, m_currentRotation);
    m_gridWidget = gridW;
    outerLayout->addWidget(m_gridWidget, 0, Qt::AlignHCenter | Qt::AlignTop);

    // Middle: three-column area (stats | module buttons | blueprints).
    QHBoxLayout* columnsLayout = new QHBoxLayout();

    // Left column: ship stats panel, and beneath it what the layout being configured
    // would cost to build (REQ-MOD-UI-DIALOG). The cost sits outside the stats panel
    // because that widget is shared with the live ship card and the balancing tool,
    // neither of which costs anything -- and the balancing tool is deliberately built
    // without the item icons this line draws with.
    QVBoxLayout* leftLayout = new QVBoxLayout();
    m_statsPanel = new ShipStatsPanel(m_config, this);
    m_statsPanel->setDebugDrawEnabled(m_debugDraw);
    leftLayout->addWidget(m_statsPanel);

    m_buildCostSection = new SectionBox(tr("Build cost"), this);
    m_buildCostLine    = new RecipeLineRow(m_itemIcons, nullptr, m_buildCostSection);
    // A section of the dialog rather than a control, so its materials explain themselves
    // on hover or click, as they do in the panel (REQ-UI-ITEM-VALUE-TOOLTIP).
    m_buildCostLine->setItemTooltips(m_context, TooltipTrigger::Trigger::HoverAndClick);
    m_buildCostSection->getContentLayout()->addWidget(m_buildCostLine);
    leftLayout->addWidget(m_buildCostSection);
    leftLayout->addStretch(1);
    columnsLayout->addLayout(leftLayout);

    // Center column: module selection buttons.
    QVBoxLayout* centerLayout = new QVBoxLayout();

    QGridLayout* buttonGrid = new QGridLayout();
    buttonGrid->setSpacing(4);

    QSignalMapper* mapper = new QSignalMapper(this);
    int col = 0;
    int row = 0;
    const int kCols = 2;

    for (int i = 0; i < static_cast<int>(m_config->modules.modules.size()); ++i)
    {
        const ModuleDef& def = m_config->modules.modules[i];
        if (m_unlockedModuleIds.count(def.id) == 0)
        {
            m_moduleButtons.push_back(nullptr);
            continue;
        }
        // The module's name over what it costs -- its materials and the production time
        // it adds (REQ-MOD-UI-DIALOG). The glyph is how a module is identified on the
        // layout grid, not here. Both children are transparent to the mouse so a click
        // anywhere on the face still reaches the button.
        OptionButton* btn = new OptionButton(this);
        btn->setCheckable(true);

        QVBoxLayout* face = new QVBoxLayout(btn);
        face->setContentsMargins(8, 4, 8, 4);
        face->setSpacing(1);

        QLabel* nameLabel =
            new QLabel(QString::fromStdString(toDisplayName(def.id)), btn);
        nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
        face->addWidget(nameLabel);

        RecipeLineRow::Spec cost;
        cost.inputs             = toAmounts(def.materials);
        cost.durationSeconds    = def.productionTimeSeconds;
        cost.durationIsAddition = true;

        RecipeLineRow* costLine = new RecipeLineRow(m_itemIcons, nullptr, btn);
        costLine->setAttribute(Qt::WA_TransparentForMouseEvents, true);
        face->addWidget(costLine);
        costLine->setLine(cost);

        // The config tooltip stays: it says what the module does, which the cost line
        // does not (REQ-MOD-UI-MODULE-TOOLTIP). Hover only: the click selects the module
        // (REQ-UI-TOOLTIP-TRIGGER). The materials on the face stay silent whether or not
        // this module fills its description in -- the button is a described control, and
        // one module's config gap must not make it read differently from the one beside
        // it (REQ-UI-ITEM-VALUE-TOOLTIP).
        if (def.tooltip)
        {
            TooltipTrigger::attachText(*btn, QString::fromStdString(*def.tooltip),
                                       TooltipTrigger::Trigger::HoverOnly);
        }
        buttonGrid->addWidget(btn, row, col);
        m_moduleButtons.push_back(btn);

        mapper->setMapping(btn, i);
        connect(btn, &QPushButton::clicked, mapper, qOverload<>(&QSignalMapper::map));

        ++col;
        if (col >= kCols)
        {
            col = 0;
            ++row;
        }
    }
    connect(mapper, qOverload<int>(&QSignalMapper::mapped),
            this, &ShipLayoutDialog::onModuleButtonClicked);

    // Remove button.
    m_removeButton = new QPushButton(tr("Remove"), this);
    m_removeButton->setCheckable(true);
    m_removeButton->setFixedHeight(48);
    if (col > 0)
    {
        ++row;
    }
    buttonGrid->addWidget(m_removeButton, row, 0, 1, kCols);
    connect(m_removeButton, &QPushButton::clicked,
            this, &ShipLayoutDialog::onRemoveButtonClicked);

    centerLayout->addLayout(buttonGrid);
    centerLayout->addStretch();

    columnsLayout->addLayout(centerLayout);

    // Right column: blueprint panel.
    ShipLayoutBlueprintPanel* bpPanel = new ShipLayoutBlueprintPanel(
        allBlueprints,
        m_shipId,
        [this]() { return m_placedModules; },
        [this](const std::vector<PlacedModule>& mods) { loadLayoutBlueprint(mods); },
        this);
    columnsLayout->addWidget(bpPanel);

    outerLayout->addLayout(columnsLayout, 1);

    // Bottom: confirm / cancel buttons.
    QHBoxLayout* bottomBar = new QHBoxLayout();
    QPushButton* confirmBtn = new QPushButton(tr("Confirm"), this);
    QPushButton* cancelBtn  = new QPushButton(tr("Cancel"), this);
    bottomBar->addWidget(confirmBtn);
    bottomBar->addWidget(cancelBtn);
    outerLayout->addLayout(bottomBar);

    connect(confirmBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onConfirm);
    connect(cancelBtn,  &QPushButton::clicked, this, &ShipLayoutDialog::onCancel);

    // Initial stats display.
    updateStats();

    // Grid click handler.
    connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) {
        if (!m_removeMode && !m_activeModuleIndex.has_value())
        {
            return;
        }

        if (m_removeMode)
        {
            // Remove mode: find and remove module at cell.
            if (cell.y() >= 0 && cell.y() < m_rows && cell.x() >= 0 && cell.x() < m_cols)
            {
                const std::optional<int> idx = m_grid[cell.y()][cell.x()].moduleIndex;
                if (idx.has_value())
                {
                    m_placedModules.erase(m_placedModules.begin() + *idx);
                    rebuildOccupancy();
                    updateGridWidget();
                    updateStats();
                }
            }
            return;
        }

        // Place module.
        const ModuleDef& def = m_config->modules.modules[*m_activeModuleIndex];
        if (canPlaceModule(def, cell, m_currentRotation))
        {
            PlacedModule pm;
            pm.moduleId = def.id;
            pm.position = cell;
            pm.rotation = m_currentRotation;
            m_placedModules.push_back(pm);
            rebuildOccupancy();
            updateGridWidget();
            updateStats();
        }
    });
}

std::optional<ShipLayoutConfig> ShipLayoutDialog::getResult() const
{
    return m_result;
}

void ShipLayoutDialog::keyPressEvent(QKeyEvent* event)
{
    if (event->key() == Qt::Key_R)
    {
        if (event->modifiers() & Qt::ShiftModifier)
        {
            // Shift+R: rotate CW.
            switch (m_currentRotation)
            {
                case Rotation::East:  m_currentRotation = Rotation::South; break;
                case Rotation::South: m_currentRotation = Rotation::West;  break;
                case Rotation::West:  m_currentRotation = Rotation::North; break;
                case Rotation::North: m_currentRotation = Rotation::East;  break;
            }
        }
        else
        {
            // R: rotate CCW.
            switch (m_currentRotation)
            {
                case Rotation::East:  m_currentRotation = Rotation::North; break;
                case Rotation::North: m_currentRotation = Rotation::West;  break;
                case Rotation::West:  m_currentRotation = Rotation::South; break;
                case Rotation::South: m_currentRotation = Rotation::East;  break;
            }
        }
        updateGridWidget();
    }
    else
    {
        // Q among them, which the base turns into requestDismiss().
        ModalDialog::keyPressEvent(event);
    }
}

bool ShipLayoutDialog::isDismissible() const
{
    return true;
}

void ShipLayoutDialog::requestDismiss()
{
    // Both mode exits go through the handler the buttons use, so a dismissal and a
    // button can never leave different state behind (REQ-MOD-PLACEMENT, REQ-MOD-REMOVE).
    if (m_activeModuleIndex.has_value()) { onModuleButtonClicked(*m_activeModuleIndex); }
    else if (m_removeMode)               { onRemoveButtonClicked(); }
    else                                 { onCancel(); }
}

void ShipLayoutDialog::onModuleButtonClicked(int index)
{
    if (m_activeModuleIndex == index)
    {
        if (m_moduleButtons[index]) { m_moduleButtons[index]->setChecked(false); }
        m_activeModuleIndex = std::nullopt;
    }
    else
    {
        for (int i = 0; i < static_cast<int>(m_moduleButtons.size()); ++i)
        {
            if (m_moduleButtons[i]) { m_moduleButtons[i]->setChecked(i == index); }
        }
        m_removeButton->setChecked(false);
        m_removeMode = false;
        m_activeModuleIndex = index;
    }
    updateGridWidget();
}

void ShipLayoutDialog::onRemoveButtonClicked()
{
    if (m_removeMode)
    {
        m_removeMode = false;
        m_removeButton->setChecked(false);
    }
    else
    {
        for (QPushButton* btn : m_moduleButtons)
        {
            if (btn) { btn->setChecked(false); }
        }
        m_activeModuleIndex = std::nullopt;
        m_removeMode = true;
        m_removeButton->setChecked(true);
    }
    updateGridWidget();
}

void ShipLayoutDialog::onConfirm()
{
    ShipLayoutConfig layout;
    layout.placedModules = m_placedModules;
    m_result = layout;
    accept();
}

void ShipLayoutDialog::onCancel()
{
    m_result = std::nullopt;
    reject();
}

void ShipLayoutDialog::rebuildOccupancy()
{
    for (int r = 0; r < m_rows; ++r)
    {
        for (int c = 0; c < m_cols; ++c)
        {
            m_grid[r][c].moduleIndex = std::nullopt;
        }
    }

    for (int i = 0; i < static_cast<int>(m_placedModules.size()); ++i)
    {
        const PlacedModule& pm = m_placedModules[i];
        const ModuleDef* def = m_config->modules.findModuleDef(pm.moduleId);
        if (!def)
        {
            continue;
        }
        const std::vector<std::string> mask = rotatedMask(*def, pm.rotation);
        for (int mr = 0; mr < static_cast<int>(mask.size()); ++mr)
        {
            for (int mc = 0; mc < static_cast<int>(mask[mr].size()); ++mc)
            {
                if (mask[mr][mc] != 'O')
                {
                    continue;
                }
                const int gr = pm.position.y() + mr;
                const int gc = pm.position.x() + mc;
                if (gr >= 0 && gr < m_rows && gc >= 0 && gc < m_cols)
                {
                    m_grid[gr][gc].moduleIndex = i;
                }
            }
        }
    }
}

void ShipLayoutDialog::updateGridWidget()
{
    LayoutGridWidget* gridW = static_cast<LayoutGridWidget*>(m_gridWidget);
    gridW->setGhostData(m_activeModuleIndex, m_currentRotation);
    gridW->update();
}

void ShipLayoutDialog::updateStats()
{
    m_statsPanel->refresh(m_shipId, m_placedModules);

    // What the layout as it currently stands would cost to build, so the price follows
    // every placement and removal (REQ-MOD-UI-DIALOG). The sums come from the
    // simulation's own rules rather than being added up a second time here
    // (REQ-MOD-MATERIALS, REQ-MOD-PRODUCTION-TIME).
    ShipLayoutConfig layout;
    layout.placedModules = m_placedModules;
    const std::map<std::string, int> materials =
        computeShipyardRequiredMaterials(*m_config, m_shipId, layout);

    RecipeLineRow::Spec cost;
    cost.inputs.reserve(materials.size());
    for (const std::pair<const std::string, int>& entry : materials)
    {
        cost.inputs.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
    }
    cost.durationSeconds =
        computeShipyardProductionTimeSeconds(*m_config, m_shipId, layout);

    m_buildCostLine->setLine(cost);
    // A schematic costing no materials at all has no line to draw, and the section then
    // stays out of the way.
    m_buildCostSection->setVisible(!cost.inputs.empty());
}

bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,
                                      Rotation rotation) const
{
    const std::vector<std::string> mask = rotatedMask(def, rotation);
    for (int mr = 0; mr < static_cast<int>(mask.size()); ++mr)
    {
        for (int mc = 0; mc < static_cast<int>(mask[mr].size()); ++mc)
        {
            if (mask[mr][mc] != 'O')
            {
                continue;
            }
            const int gr = position.y() + mr;
            const int gc = position.x() + mc;
            if (gr < 0 || gr >= m_rows || gc < 0 || gc >= m_cols)
            {
                return false;
            }
            if (!m_grid[gr][gc].buildable)
            {
                return false;
            }
            if (m_grid[gr][gc].moduleIndex.has_value())
            {
                return false;
            }
        }
    }
    return true;
}

std::vector<std::string> ShipLayoutDialog::rotatedMask(const ModuleDef& def,
                                                       Rotation rotation) const
{
    int steps = 0;
    switch (rotation)
    {
        case Rotation::East:  steps = 0; break;
        case Rotation::South: steps = 1; break;
        case Rotation::West:  steps = 2; break;
        case Rotation::North: steps = 3; break;
    }
    std::vector<std::string> result = def.surfaceMask;
    for (int i = 0; i < steps; ++i)
    {
        result = rotateMaskCW(result);
    }
    return result;
}

void ShipLayoutDialog::loadLayoutBlueprint(const std::vector<PlacedModule>& modules)
{
    m_placedModules.clear();

    // Build a temporary occupancy grid to detect overlaps within the blueprint.
    std::vector<std::vector<bool>> occupied(m_rows, std::vector<bool>(m_cols, false));

    for (const PlacedModule& pm : modules)
    {
        // Validate module type exists and is unlocked.
        const ModuleDef* def = m_config->modules.findModuleDef(pm.moduleId);
        if (!def || m_unlockedModuleIds.count(def->id) == 0) { continue; }

        const std::vector<std::string> mask = rotatedMask(*def, pm.rotation);
        bool valid = true;

        for (int mr = 0; mr < static_cast<int>(mask.size()) && valid; ++mr)
        {
            for (int mc = 0; mc < static_cast<int>(mask[mr].size()) && valid; ++mc)
            {
                if (mask[mr][mc] != 'O') { continue; }
                const int gr = pm.position.y() + mr;
                const int gc = pm.position.x() + mc;
                if (gr < 0 || gr >= m_rows || gc < 0 || gc >= m_cols) { valid = false; break; }
                if (!m_grid[gr][gc].buildable) { valid = false; break; }
                if (occupied[gr][gc]) { valid = false; break; }
            }
        }

        if (!valid) { continue; }

        // Mark cells as occupied.
        for (int mr = 0; mr < static_cast<int>(mask.size()); ++mr)
        {
            for (int mc = 0; mc < static_cast<int>(mask[mr].size()); ++mc)
            {
                if (mask[mr][mc] != 'O') { continue; }
                const int gr = pm.position.y() + mr;
                const int gc = pm.position.x() + mc;
                if (gr >= 0 && gr < m_rows && gc >= 0 && gc < m_cols)
                {
                    occupied[gr][gc] = true;
                }
            }
        }

        m_placedModules.push_back(pm);
    }

    rebuildOccupancy();
    updateGridWidget();
    updateStats();
}
