#include "BuildButtonBar.h"

#include <string>

#include <QColor>
#include <QFont>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QIcon>
#include <QPainter>
#include <QPalette>
#include <QPixmap>
#include <QPushButton>
#include <QRect>
#include <QSignalMapper>
#include <QSize>
#include <QString>

#include "BuildingIconCache.h"
#include "BuildingType.h"
#include "BuildingTypeSelectedEvent.h"
#include "DeconstructModeToggleRequestedEvent.h"
#include "DisplayName.h"
#include "EventManager.h"
#include "ExitBuilderModeRequestedEvent.h"
#include "FloatingLayoutInvalidatedEvent.h"
#include "IconCaption.h"
#include "InputMapper.h"
#include "ItemIconCache.h"
#include "Simulation.h"
#include "TooltipTrigger.h"

namespace
{
    // Size the SVG chips are drawn at on a button face, in device-independent pixels.
    const QSize kIconSize(32, 32);

    // Minimum width of a button face. The building-type buttons all come out at this
    // width, so the row is uniform; a face whose caption is wider than this — the
    // Deconstruct button's name — grows to fit rather than clipping
    // (REQ-UI-DECONSTRUCT-BUTTON).
    const int kFaceMinWidthPx = 48;

    // Gap between the last building button and the Deconstruct button, which toggles
    // a mode rather than selecting a building type (REQ-UI-DECONSTRUCT-BUTTON).
    const int kDeconstructGapPx = 16;

    // Distance from the bar to the bottom edge of the game world view
    // (REQ-UI-BUILD-BAR).
    const int kBottomMarginPx = 8;

    // Gap between the chip icon and the cost line on a button face.
    const int kFaceGapPx = 2;

    // Normal and grey-background chip pixmaps for one icon name. Empty pixmaps if the
    // file cannot be read; the caller then falls back to a name caption
    // (REQ-UI-BUILD-ICON).
    struct ChipPixmaps { QPixmap normal; QPixmap grey; };
    ChipPixmaps loadChipPixmaps(BuildingIconCache& icons, const std::string& iconName)
    {
        ChipPixmaps result;
        result.normal = icons.getChip(iconName, kIconSize.width());
        result.grey   = icons.getGreyChip(iconName, kIconSize.width());
        return result;
    }

    // One button face: the hotkey badge in the top-left corner, the chip icon centered
    // below it, and the caption — the cost, or the Deconstruct name — centered at the
    // bottom (REQ-UI-BUILD-COST). The three are composed into a single pixmap because
    // a QPushButton holds only one icon.
    QPixmap composeButtonFace(const QString& hotkeyLabel, const QPixmap& chip,
                              const QPixmap& caption, const QFont& badgeFont,
                              const QColor& badgeColor)
    {
        const QSize chipSize    = getLogicalSize(chip);
        const QSize captionSize = getLogicalSize(caption);
        // The badge row is kept even for a building type without a hotkey, so buttons
        // stay the same height and the row reads as one strip (REQ-UI-BUILD-COST).
        const int badgeHeight = QFontMetrics(badgeFont).height();
        const int width  = qMax(kFaceMinWidthPx, qMax(chipSize.width(), captionSize.width()));
        const int height = badgeHeight + chipSize.height() + kFaceGapPx + captionSize.height();

        const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
        QPixmap face(static_cast<int>(width * dpr), static_cast<int>(height * dpr));
        face.setDevicePixelRatio(dpr);
        face.fill(Qt::transparent);

        QPainter painter(&face);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
        if (!hotkeyLabel.isEmpty())
        {
            painter.setFont(badgeFont);
            painter.setPen(badgeColor);
            painter.drawText(QRect(0, 0, width, badgeHeight),
                             Qt::AlignLeft | Qt::AlignVCenter, hotkeyLabel);
        }
        painter.drawPixmap((width - chipSize.width()) / 2, badgeHeight, chip);
        painter.drawPixmap((width - captionSize.width()) / 2,
                           badgeHeight + chipSize.height() + kFaceGapPx, caption);
        return face;
    }

    // A composed button face and the size to show it at; the button needs both, and
    // only the composer knows the size it arrived at.
    struct ButtonFace { QIcon icon; QSize size; };

    // The two-mode face of one build button. The modes differ only in the chip variant
    // and the text color, so a disabled (unaffordable) button greys itself when Qt
    // swaps the pixmap, with no extra work in updateAffordability()
    // (REQ-UI-BUILD-DISABLED).
    ButtonFace buildButtonFace(const ChipPixmaps& chip, const QString& hotkeyLabel,
                               const QString& name, const QString& captionText,
                               const QPixmap& blockIcon, const QFont& font,
                               const QPalette& palette)
    {
        // Full button size and bold: at a smaller size the badge was hard to read and
        // its arrow glyph illegible. It stays dimmed in both modes instead, so it
        // reads as a reminder without competing with the cost.
        QFont badgeFont = font;
        badgeFont.setBold(true);
        const QColor badgeColor = palette.color(QPalette::Disabled, QPalette::ButtonText);

        ButtonFace result;
        for (QIcon::Mode mode : { QIcon::Normal, QIcon::Disabled })
        {
            const bool enabled = (mode == QIcon::Normal);
            const QColor textColor = palette.color(
                enabled ? QPalette::Active : QPalette::Disabled, QPalette::ButtonText);
            const QPixmap& chipPixmap = enabled ? chip.normal : chip.grey;
            // A missing "<id>.svg" puts the building name where the chip would be, so
            // the button stays identifiable in an icon-only bar (REQ-UI-BUILD-ICON).
            const QPixmap middle = chipPixmap.isNull()
                ? renderCaptionWithIcon(name, QPixmap(), font, textColor)
                : chipPixmap;
            const QPixmap caption =
                renderCaptionWithIcon(captionText, blockIcon, font, textColor);
            const QPixmap face =
                composeButtonFace(hotkeyLabel, middle, caption, badgeFont, badgeColor);
            result.icon.addPixmap(face, mode);
            // Both modes compose to the same size; keeping the larger is only a guard
            // against a fallback name caption widening one of them.
            result.size = result.size.expandedTo(getLogicalSize(face));
        }
        return result;
    }

}


BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
                               BuildingIconCache* buildingIcons,
                               ItemIconCache* itemIcons, QWidget* parent)
    : QWidget(parent)
    , m_sim(sim)
    , m_config(config)
    , m_buildingIcons(buildingIcons)
    , m_itemIcons(itemIcons)
{
    // The bar floats over the rendered world rather than sitting in a panel, so it
    // brings its own opaque background to stay legible over any world content
    // (REQ-UI-BUILD-BAR). Palette colors keep it consistent with the buttons it holds
    // and with the selection panel; this is widget chrome, not world rendering, so it is
    // deliberately not a visuals.toml color.
    setAttribute(Qt::WA_StyledBackground, true);
    setStyleSheet(QStringLiteral(
        "BuildButtonBar { background-color: palette(window);"
        " border: 1px solid palette(mid); border-radius: 4px; }"));

    QHBoxLayout* layout = new QHBoxLayout(this);
    layout->setSpacing(4);
    layout->setContentsMargins(6, 4, 6, 4);

    QSignalMapper* mapper = new QSignalMapper(this);

    // Block icon shown to the right of each button's cost (REQ-UI-BUILD-COST); null
    // when no building_block icon exists, in which case the cost is the bare number.
    const QPixmap blockIcon = m_itemIcons->getInlineIcon(kBlockItemId, font());

    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-BAR). 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-ICON); Tunnel Entry's
        // "tunnel_entry.svg" serves the shared Tunnel button.
        const ButtonFace face = buildButtonFace(
            loadChipPixmaps(*m_buildingIcons, def.id),
            InputMapper::getBuildHotkeyLabel(def.type), name,
            QString::number(def.cost), blockIcon, font(), palette());

        QPushButton* btn = new QPushButton(this);
        btn->setIcon(face.icon);
        btn->setIconSize(face.size);
        btn->setCheckable(true);
        // The button face carries no name (REQ-UI-BUILD-COST), so the tooltip always
        // leads with it and adds the config description when there is one
        // (REQ-UI-BUILD-TOOLTIP). Hover only: the click enters builder mode
        // (REQ-UI-TOOLTIP-TRIGGER).
        TooltipTrigger::attachText(*btn, def.tooltip
            ? QStringLiteral("%1\n%2").arg(name, QString::fromStdString(*def.tooltip))
            : name,
            TooltipTrigger::Trigger::HoverOnly);
        layout->addWidget(btn);

        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));
    }
    connect(mapper, qOverload<int>(&QSignalMapper::mapped), this, &BuildButtonBar::onBuildButton);

    // Set apart from the building-type buttons by a gap, because it toggles a mode
    // rather than selecting a building type (REQ-UI-DECONSTRUCT-BUTTON). A fixed
    // spacer rather than a stretch: the bar is sized to its contents, so there is no
    // right edge for a stretch to push against.
    layout->addSpacing(kDeconstructGapPx);

    // Having no cost, it shows its name where the building buttons show theirs
    // (REQ-UI-DECONSTRUCT-BUTTON), and its Q toggle as the badge (REQ-UI-HOTKEYS).
    const ButtonFace deconstructFace = buildButtonFace(
        loadChipPixmaps(*m_buildingIcons, "deconstruct"),
        QStringLiteral("Q"), tr("Deconstruct"), tr("Deconstruct"), QPixmap(),
        font(), palette());

    m_deconstructButton = new QPushButton(this);
    m_deconstructButton->setCheckable(true);
    m_deconstructButton->setIcon(deconstructFace.icon);
    m_deconstructButton->setIconSize(deconstructFace.size);
    // 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);
    // Hover only: the click toggles the mode (REQ-UI-TOOLTIP-TRIGGER).
    TooltipTrigger::attachText(*m_deconstructButton, deconstructTooltip,
                               TooltipTrigger::Trigger::HoverOnly);
    layout->addWidget(m_deconstructButton);
    connect(m_deconstructButton, &QPushButton::clicked, this, [this]() {
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<DeconstructModeToggleRequestedEvent>());
    });

    updateVisibility();
    registerForEvents();
}

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

void BuildButtonBar::placeIn(const QRect& viewRect,
                             const std::vector<QRect>& /*occupiedRects*/)
{
    if (viewRect.isNull())
    {
        return;
    }
    // The layout drops hidden buttons from its size hint, but only once it has been
    // re-run: an unlock changes which buttons are shown, and Qt would not get around to
    // it before the bar is measured here.
    layout()->activate();

    const QSize barSize = sizeHint();
    // Centered, except that a bar wider than the view stays flush with its left edge
    // rather than hanging off both sides.
    const int x = qMax(viewRect.left(),
                       viewRect.left() + (viewRect.width() - barSize.width()) / 2);
    const int y = viewRect.bottom() - kBottomMarginPx - barSize.height() + 1;
    setGeometry(QRect(QPoint(x, y), barSize));
}

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

void BuildButtonBar::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 BuildButtonBar::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]));
    }
    // A hidden button leaves the row, so the bar takes up a new width and has to be
    // re-centered on it -- and the widgets that keep clear of the bar have to be placed
    // against that new rect too, so the whole pass is re-run (REQ-UI-BUILD-BAR).
    EventManager::getInstance()->sendEventImmediately(
        std::make_shared<FloatingLayoutInvalidatedEvent>());
}

void BuildButtonBar::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 BuildButtonBar::handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/)
{
    clearActiveButton();
}

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

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

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

void BuildButtonBar::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;
        }
    }
}
