#include "BuildButtonGrid.h"

#include <string>

#include <QByteArray>
#include <QColor>
#include <QFile>
#include <QFontMetrics>
#include <QGridLayout>
#include <QIcon>
#include <QPainter>
#include <QPaintEvent>
#include <QPalette>
#include <QPixmap>
#include <QPushButton>
#include <QRect>
#include <QRegularExpression>
#include <QSignalMapper>
#include <QSize>
#include <QString>
#include <QSvgRenderer>

#include "BuildingType.h"
#include "BuildingTypeSelectedEvent.h"
#include "DeconstructModeToggleRequestedEvent.h"
#include "DisplayName.h"
#include "EventManager.h"
#include "ExitBuilderModeRequestedEvent.h"
#include "ItemIconCache.h"
#include "Simulation.h"

namespace
{
    // Pixel size the SVG chips are rasterized at; downscaled to the button icon size.
    const int kIconRenderSize = 64;
    const QSize kIconSize(28, 28);

    QPixmap renderChip(const QByteArray& svg)
    {
        QSvgRenderer renderer(svg);
        QPixmap pixmap(kIconRenderSize, kIconRenderSize);
        pixmap.fill(Qt::transparent);
        QPainter painter(&pixmap);
        renderer.render(&painter);
        return pixmap;
    }

    // Builds a build-button icon from a "<id>.svg" chip file. The returned QIcon also
    // carries a Disabled-mode pixmap whose chip background is recolored grey, so an
    // unaffordable (disabled) button shows the grey variant automatically
    // (REQ-UI-BUILD-DISABLED) without any extra work in updateAffordability().
    QIcon loadBuildingIcon(const QString& path)
    {
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly))
        {
            return QIcon();
        }
        const QByteArray svg = file.readAll();

        QIcon icon;
        icon.addPixmap(renderChip(svg), QIcon::Normal);

        // Recolor only the chip background: the first "#rrggbb" fill in the file is the
        // rounded background rect; the white glyph uses fill="none" and is left alone.
        QString greyed = QString::fromUtf8(svg);
        static const QRegularExpression fillPattern(QStringLiteral("fill=\"#[0-9a-fA-F]{6}\""));
        const QRegularExpressionMatch match = fillPattern.match(greyed);
        if (match.hasMatch())
        {
            greyed.replace(match.capturedStart(), match.capturedLength(),
                           QStringLiteral("fill=\"#5f636e\""));
        }
        icon.addPixmap(renderChip(greyed.toUtf8()), QIcon::Disabled);
        return icon;
    }

    // Normal and grey-background chip pixmaps for a "<id>.svg" file, using the same
    // recolor rule as loadBuildingIcon. Empty pixmaps if the file cannot be read.
    struct ChipPixmaps { QPixmap normal; QPixmap grey; };
    ChipPixmaps loadChipPixmaps(const QString& path)
    {
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly)) { return {}; }
        const QByteArray svg = file.readAll();

        ChipPixmaps result;
        result.normal = renderChip(svg);
        QString greyed = QString::fromUtf8(svg);
        static const QRegularExpression fillPattern(QStringLiteral("fill=\"#[0-9a-fA-F]{6}\""));
        const QRegularExpressionMatch match = fillPattern.match(greyed);
        if (match.hasMatch())
        {
            greyed.replace(match.capturedStart(), match.capturedLength(),
                           QStringLiteral("fill=\"#5f636e\""));
        }
        result.grey = renderChip(greyed.toUtf8());
        return result;
    }

    // A build button that paints its own face — chip icon at the left, the building
    // name above the cost, and the building_block item icon after the cost number in
    // place of the "Blocks" word (REQ-UI-BUILD-COST, REQ-UI-BUILD-ICON). Custom paint
    // (rather than the native icon+text) is needed because a QPushButton holds only
    // one icon; this stays adaptive to the button width and greys itself when the
    // button is disabled/unaffordable (REQ-UI-BUILD-DISABLED).
    class BuildButton : public QPushButton
    {
    public:
        BuildButton(const ChipPixmaps& chip, const QString& name,
                    const QString& costText, const QPixmap& blockIcon, QWidget* parent)
            : QPushButton(parent)
            , m_chip(chip)
            , m_name(name)
            , m_costText(costText)
            , m_blockIcon(blockIcon)
        {
        }

    protected:
        void paintEvent(QPaintEvent* event) override
        {
            QPushButton::paintEvent(event);  // frame, checked/hover state

            QPainter painter(this);
            painter.setRenderHint(QPainter::Antialiasing, true);
            painter.setRenderHint(QPainter::SmoothPixmapTransform, true);

            const bool on = isEnabled();
            const QRect area = rect().adjusted(6, 4, -6, -4);
            const int chipSize = kIconSize.width();
            const QPixmap& chip = on ? m_chip.normal : m_chip.grey;
            if (!chip.isNull())
            {
                painter.drawPixmap(
                    QRect(area.x(), area.y() + (area.height() - chipSize) / 2,
                          chipSize, chipSize), chip);
            }

            const QRect textArea(area.x() + chipSize + 6, area.y(),
                                 area.width() - chipSize - 6, area.height());
            const QFontMetrics metrics(font());
            const int lineHeight = metrics.height();
            painter.setFont(font());
            painter.setPen(palette().color(on ? QPalette::Active : QPalette::Disabled,
                                           QPalette::ButtonText));

            // Name fills everything above the bottom cost line (word-wrapped).
            painter.drawText(
                QRect(textArea.x(), textArea.y(),
                      textArea.width(), textArea.height() - lineHeight),
                Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, m_name);

            // Cost line: "<n>" then the block icon (or "<n> Blocks" when no icon).
            const int costY = textArea.bottom() - lineHeight + 1;
            if (m_blockIcon.isNull())
            {
                painter.drawText(
                    QRect(textArea.x(), costY, textArea.width(), lineHeight),
                    Qt::AlignLeft | Qt::AlignVCenter,
                    QObject::tr("%1 Blocks").arg(m_costText));
                return;
            }
            const int costWidth = metrics.horizontalAdvance(m_costText);
            painter.drawText(QRect(textArea.x(), costY, costWidth, lineHeight),
                             Qt::AlignLeft | Qt::AlignVCenter, m_costText);
            const qreal iconDpr = m_blockIcon.devicePixelRatio();
            const int iconW = static_cast<int>(m_blockIcon.width() / iconDpr);
            const int iconH = static_cast<int>(m_blockIcon.height() / iconDpr);
            if (!on) { painter.setOpacity(0.45); }
            painter.drawPixmap(
                QRect(textArea.x() + costWidth + 4, costY + (lineHeight - iconH) / 2,
                      iconW, iconH), m_blockIcon);
        }

    private:
        ChipPixmaps m_chip;
        QString     m_name;
        QString     m_costText;
        QPixmap     m_blockIcon;
    };
}


BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
                                 const std::string& iconDir,
                                 ItemIconCache* itemIcons, QWidget* parent)
    : QWidget(parent)
    , m_sim(sim)
    , m_config(config)
    , m_iconDir(iconDir)
    , m_itemIcons(itemIcons)
{
    QGridLayout* layout = new QGridLayout(this);
    layout->setSpacing(4);
    layout->setContentsMargins(4, 4, 4, 4);

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

    // Block icon shown in each button's cost line (REQ-UI-BUILD-COST); null when no
    // building_block icon exists, in which case buttons fall back to text costs.
    const bool hasBlockIcon = m_itemIcons->hasIcon("building_block");
    const QPixmap blockIcon = hasBlockIcon
        ? m_itemIcons->getPixmap("building_block", QFontMetrics(font()).height())
        : QPixmap();

    for (const BuildingDef& def : config->buildings.buildings)
    {
        if (!def.playerPlaceable)
        {
            continue;
        }
        // Tunnel Entry and Tunnel Exit share a single "Tunnel" button; the exit is
        // reached through the unified tunnel build mode, not its own button
        // (REQ-BLD-TUNNEL-MODE, REQ-UI-BUILD-GRID). Both stay player-placeable so
        // blueprints and cost totals still account for exits.
        if (def.type == BuildingType::TunnelExit)
        {
            continue;
        }
        m_types.push_back(def.type);
        m_costs[def.type] = def.cost;

        const QString name = (def.type == BuildingType::TunnelEntry)
            ? tr("Tunnel")
            : QString::fromStdString(toDisplayName(def.id));
        // Icon file name matches the building id (REQ-UI-BUILD-GRID); Tunnel Entry's
        // "tunnel_entry.svg" serves the shared Tunnel button.
        const QString iconPath = QString::fromStdString(m_iconDir) + "/"
            + QString::fromStdString(def.id) + ".svg";

        QPushButton* btn = nullptr;
        if (hasBlockIcon)
        {
            // Custom-painted button showing the cost with the block icon in place of
            // the "Blocks" word (REQ-UI-BUILD-COST).
            btn = new BuildButton(loadChipPixmaps(iconPath), name,
                                  QString::number(def.cost), blockIcon, this);
        }
        else
        {
            // Fallback: native chip icon + text cost when no block icon exists.
            btn = new QPushButton(name + "\n" + tr("%1 Blocks").arg(def.cost), this);
            btn->setIcon(loadBuildingIcon(iconPath));
            btn->setIconSize(kIconSize);
        }
        btn->setCheckable(true);
        btn->setFixedHeight(48);
        if (def.tooltip)
        {
            btn->setToolTip(QString::fromStdString(*def.tooltip));
        }
        layout->addWidget(btn, row, col);

        const int idx = static_cast<int>(m_buttons.size());
        m_buttons.push_back(btn);
        mapper->setMapping(btn, idx);
        connect(btn, &QPushButton::clicked, mapper, qOverload<>(&QSignalMapper::map));

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

    m_deconstructButton = new QPushButton(tr("Deconstruct"), this);
    m_deconstructButton->setCheckable(true);
    m_deconstructButton->setFixedHeight(48);
    m_deconstructButton->setIcon(loadBuildingIcon(
        QString::fromStdString(m_iconDir) + "/deconstruct.svg"));
    m_deconstructButton->setIconSize(kIconSize);
    // Refund tooltip composed from world.refund_percentage (REQ-UI-DECONSTRUCT-BUTTON,
    // REQ-BLD-DECONSTRUCT). A finished building refunds the configured percentage; a
    // construction site removed before it is built is refunded in full. When the
    // percentage is 100% both cases coincide, so the tooltip is simplified to one case.
    const int refundPercentage = m_config->world.refundPercentage;
    const QString deconstructTooltip = (refundPercentage >= 100)
        ? tr("Deconstruct buildings. Refunds %1% of the building block cost.")
              .arg(refundPercentage)
        : tr("Deconstruct buildings. A finished building refunds %1% of its building "
             "block cost once removed; a construction site removed before it is built "
             "is refunded in full.")
              .arg(refundPercentage);
    m_deconstructButton->setToolTip(deconstructTooltip);
    layout->addWidget(m_deconstructButton, row, col);
    connect(m_deconstructButton, &QPushButton::clicked, this, [this]() {
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<DeconstructModeToggleRequestedEvent>());
    });

    updateVisibility();
    registerForEvents();
}

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

void BuildButtonGrid::updateAffordability()
{
    const int buildingBlocks = m_sim->getBuildingBlocksStock();

    // If the currently selected tool can no longer be afforded, exit builder mode
    // before recomputing button states so it does not stay selected. Clearing the
    // active index first lets the loop below disable the now-unaffordable button.
    if (m_activeIndex)
    {
        const BuildingType activeType = m_types[*m_activeIndex];
        const std::map<BuildingType, int>::const_iterator it = m_costs.find(activeType);
        const int cost = (it != m_costs.end()) ? it->second : 0;
        if (buildingBlocks < cost)
        {
            clearActiveButton();
            EventManager::getInstance()->sendEventImmediately(
                std::make_shared<ExitBuilderModeRequestedEvent>());
        }
    }

    for (std::size_t i = 0; i < m_buttons.size(); ++i)
    {
        const BuildingType type = m_types[i];
        const std::map<BuildingType, int>::const_iterator it = m_costs.find(type);
        const int cost = (it != m_costs.end()) ? it->second : 0;
        m_buttons[i]->setEnabled(buildingBlocks >= cost || m_activeIndex == i);
    }
}

void BuildButtonGrid::updateVisibility()
{
    // A locked building type's button is hidden until its unlock group is awarded
    // (REQ-LOCK-BUILDING). Buttons keep their index so hotkeys/affordability stay
    // stable; they simply appear once the type is unlocked.
    for (std::size_t i = 0; i < m_buttons.size(); ++i)
    {
        m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
    }
}

void BuildButtonGrid::clearActiveButton()
{
    if (m_activeIndex)
    {
        m_buttons[*m_activeIndex]->setChecked(false);
    }
    m_activeIndex.reset();
}

void BuildButtonGrid::onBuildButton(int index)
{
    if (index < 0 || index >= static_cast<int>(m_buttons.size()))
    {
        return;
    }
    const std::size_t idx = static_cast<std::size_t>(index);

    if (m_activeIndex == idx)
    {
        clearActiveButton();
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<ExitBuilderModeRequestedEvent>());
        return;
    }

    if (m_activeIndex)
    {
        m_buttons[*m_activeIndex]->setChecked(false);
    }

    m_activeIndex = idx;
    m_buttons[idx]->setChecked(true);
    EventManager::getInstance()->sendEventImmediately(
        std::make_shared<BuildingTypeSelectedEvent>(m_types[idx]));
}

void BuildButtonGrid::handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/)
{
    clearActiveButton();
}

void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
{
    updateAffordability();
}

void BuildButtonGrid::handleEvent(std::shared_ptr<const UnlockedBuildingsChangedEvent> /*event*/)
{
    updateVisibility();
    updateAffordability();
}

void BuildButtonGrid::handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event)
{
    m_deconstructButton->setChecked(event->active);
}

void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event)
{
    for (std::size_t i = 0; i < m_types.size(); ++i)
    {
        if (m_types[i] == event->type)
        {
            // Equivalent to clicking the build button: a disabled (unaffordable) or
            // hidden (locked, REQ-LOCK-BUILDING) button cannot be clicked, so the
            // hotkey is likewise inert.
            if (m_buttons[i]->isEnabled() && m_buttons[i]->isVisible())
            {
                onBuildButton(static_cast<int>(i));
            }
            return;
        }
    }
}
