#include "BlueprintSelectionDialog.h"

#include <cstddef>
#include <vector>

#include <QChar>
#include <QColor>
#include <QFont>
#include <QFontMetrics>
#include <QFrame>
#include <QGridLayout>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QLayoutItem>
#include <QPainter>
#include <QPalette>
#include <QPixmap>
#include <QPoint>
#include <QPushButton>
#include <QRect>
#include <QScrollArea>
#include <QSize>
#include <QString>
#include <QStyle>
#include <QVBoxLayout>

#include "Blueprint.h"
#include "BlueprintLibrary.h"
#include "IconCaption.h"
#include "ItemIconCache.h"
#include "ItemTooltip.h"
#include "TooltipTrigger.h"

namespace
{
    // Fixed at two, per REQ-UI-BLUEPRINT-DIALOG; the grid scrolls rather than reflowing.
    const int kGridColumnCount = 2;

    // Card geometry. Every card is the same size so the grid stays even
    // (REQ-UI-BLUEPRINT-CARD); the width is chosen to fit a name and a short contents
    // line, and the height follows from three text rows.
    const int kCardWidthPx    = 220;
    const int kCardPaddingPx  = 10;
    const int kCardRowGapPx   = 4;

    // Side length of the per-card delete icon and of the dialog's close button.
    const int kSmallButtonSizePx = 22;

    // Spacing between cards, and the dialog's own content margin.
    const int kSpacingPx = 8;

    // Card rows visible before the grid scrolls. The half row is deliberate: a cut-off
    // card reads as "there is more below" at a glance.
    const double kVisibleRowCount = 2.5;

    // U+00D7 MULTIPLICATION SIGN, the close and delete glyph. Written as a code point
    // because the sources are not guaranteed to be read as UTF-8 by every compiler.
    const QChar kCrossGlyph(0x00D7);

    // Inner size of a card's composed face: the name row, the contents row, and the
    // cost row, plus the gap above the cost.
    QSize getCardFaceSize(const QFont& font)
    {
        const int rowHeight = QFontMetrics(font).height();
        return QSize(kCardWidthPx - 2 * kCardPaddingPx, rowHeight * 3 + kCardRowGapPx);
    }

    QSize getCardSize(const QFont& font)
    {
        const QSize faceSize = getCardFaceSize(font);
        return QSize(faceSize.width()  + 2 * kCardPaddingPx,
                     faceSize.height() + 2 * kCardPaddingPx);
    }

    // One card face: the blueprint name, its contents line below in a dimmed color,
    // and the cost with its block icon at the bottom (REQ-UI-BLUEPRINT-CARD). The three
    // are composed into a single pixmap because a QPushButton holds only one icon --
    // the same device the build button faces use.
    QPixmap composeCardFace(const QString& name, const QString& contents,
                            const QPixmap& cost, const QFont& font,
                            const QColor& textColor, const QColor& dimColor,
                            const QSize& faceSize)
    {
        const QFontMetrics metrics(font);
        const int rowHeight = metrics.height();

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

        QPainter painter(&face);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
        painter.setFont(font);

        // Both text rows are elided rather than wrapped or shrunk, so a long name or a
        // many-typed blueprint cannot change a card's height (REQ-UI-BLUEPRINT-CARD).
        painter.setPen(textColor);
        painter.drawText(QRect(0, 0, faceSize.width(), rowHeight),
                         Qt::AlignLeft | Qt::AlignVCenter,
                         metrics.elidedText(name, Qt::ElideRight, faceSize.width()));

        painter.setPen(dimColor);
        painter.drawText(QRect(0, rowHeight, faceSize.width(), rowHeight),
                         Qt::AlignLeft | Qt::AlignVCenter,
                         metrics.elidedText(contents, Qt::ElideRight, faceSize.width()));

        const QSize costSize = getLogicalSize(cost);
        painter.drawPixmap(0, faceSize.height() - costSize.height(), cost);
        return face;
    }

    // A composed card face, the size to show it at, and where in it the cost ended up:
    // the button needs the first two, and only the composer knows the third, which is
    // what the cost's tooltip is hung on (REQ-UI-ITEM-VALUE-TOOLTIP).
    struct CardFace { QIcon icon; QSize size; QRect costRect; };

    // The two-mode face of one card. The modes differ only in the text color, so an
    // unaffordable card greys itself when Qt swaps the pixmap on the disabled button
    // (REQ-UI-BLUEPRINT-CARD, consistent with REQ-UI-BUILD-DISABLED).
    CardFace buildCardFace(const QString& name, const QString& contents, int cost,
                           const QPixmap& blockIcon, const QFont& font,
                           const QPalette& palette)
    {
        const QSize  faceSize = getCardFaceSize(font);
        const QColor dimColor = palette.color(QPalette::Disabled, QPalette::ButtonText);

        CardFace result;
        for (QIcon::Mode mode : { QIcon::Normal, QIcon::Disabled })
        {
            const QColor textColor = palette.color(
                (mode == QIcon::Normal) ? QPalette::Active : QPalette::Disabled,
                QPalette::ButtonText);
            // The cost reads exactly like the header stock and the build button costs:
            // the number, then the block icon in place of a trailing "Blocks"
            // (REQ-UI-BLOCKS-ICON). A null icon leaves the bare number.
            const QPixmap costPixmap =
                renderCaptionWithIcon(QString::number(cost), blockIcon, font, textColor);
            const QPixmap face = composeCardFace(name, contents, costPixmap, font,
                                                 textColor, dimColor, faceSize);
            result.icon.addPixmap(face, mode);
            result.size = result.size.expandedTo(getLogicalSize(face));
            // Where composeCardFace put it: bottom left, its own width. Both modes
            // compose the same text, so both arrive at the same rect.
            const QSize costSize = getLogicalSize(costPixmap);
            result.costRect = QRect(0, faceSize.height() - costSize.height(),
                                    costSize.width(), costSize.height());
        }
        return result;
    }
}

BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library,
                                                   const ItemTooltipContext& context,
                                                   QWidget* parent)
    : ModalDialog(parent)
    , m_library(library)
    , m_context(context)
    , m_itemIcons(context.itemIcons)
{
    QVBoxLayout* mainLayout = new QVBoxLayout(this);
    mainLayout->setContentsMargins(kSpacingPx, kSpacingPx, kSpacingPx, kSpacingPx);
    mainLayout->setSpacing(kSpacingPx);

    QHBoxLayout* headerLayout = addHeader(mainLayout, tr("Blueprints"), true);

    // Dimmed and bold, the treatment the build button hotkey badges use, so the
    // shortcut reads as a reminder rather than a second title (REQ-UI-BUILD-COST).
    // Inserted right after the title, left of the stretch the header ends with.
    QLabel* hotkeyBadge = new QLabel(tr("Ctrl+V"), this);
    QFont badgeFont = hotkeyBadge->font();
    badgeFont.setBold(true);
    hotkeyBadge->setFont(badgeFont);
    QPalette badgePalette = hotkeyBadge->palette();
    badgePalette.setColor(hotkeyBadge->foregroundRole(),
                          palette().color(QPalette::Disabled, QPalette::WindowText));
    hotkeyBadge->setPalette(badgePalette);
    headerLayout->insertWidget(1, hotkeyBadge);

    QScrollArea* scrollArea = new QScrollArea(this);
    scrollArea->setWidgetResizable(true);
    scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    scrollArea->setFrameShape(QFrame::NoFrame);

    m_gridContainer = new QWidget(scrollArea);
    m_grid = new QGridLayout(m_gridContainer);
    m_grid->setContentsMargins(0, 0, 0, 0);
    m_grid->setSpacing(kSpacingPx);
    m_grid->setAlignment(Qt::AlignTop | Qt::AlignLeft);
    scrollArea->setWidget(m_gridContainer);
    mainLayout->addWidget(scrollArea, 1);

    rebuildGrid();

    // Fixed size (REQ-UI-BLUEPRINT-DIALOG), sized to show kVisibleRowCount card rows.
    const QSize cardSize = getCardSize(font());
    const int gridWidth  = kGridColumnCount * cardSize.width()
        + (kGridColumnCount - 1) * kSpacingPx;
    const int gridHeight = static_cast<int>(
        cardSize.height() * kVisibleRowCount + kSpacingPx * (kVisibleRowCount - 1.0));
    setFixedSize(gridWidth + style()->pixelMetric(QStyle::PM_ScrollBarExtent)
                     + 2 * kSpacingPx,
                 gridHeight + kSmallButtonSizePx + 3 * kSpacingPx);
}

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

std::optional<int> BlueprintSelectionDialog::getChosenIndex() const
{
    return m_chosenIndex;
}

void BlueprintSelectionDialog::rebuildGrid()
{
    // deleteLater, not delete: this runs from a delete button's own clicked signal, and
    // destroying that button inside its emission would leave Qt holding a dangling
    // sender. hide() makes the removal immediate all the same.
    while (QLayoutItem* item = m_grid->takeAt(0))
    {
        if (item->widget())
        {
            item->widget()->hide();
            item->widget()->deleteLater();
        }
        delete item;
    }

    const std::vector<Blueprint>& blueprints = m_library->getBlueprints();
    if (blueprints.empty())
    {
        QLabel* emptyLabel = new QLabel(
            tr("No blueprints yet.\n\nSelect one or more buildings and press Ctrl+C "
               "to save them as a blueprint."),
            m_gridContainer);
        emptyLabel->setAlignment(Qt::AlignCenter);
        emptyLabel->setWordWrap(true);
        m_grid->addWidget(emptyLabel, 0, 0, 1, kGridColumnCount);
        return;
    }

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

    const QSize cardSize = getCardSize(font());

    for (int i = 0; i < static_cast<int>(blueprints.size()); ++i)
    {
        const Blueprint& blueprint = blueprints[static_cast<std::size_t>(i)];
        const CardFace   face      = buildCardFace(
            blueprint.name, m_library->getContentsSummary(i), m_library->getCost(i),
            blockIcon, font(), palette());

        QWidget* card = new QWidget(m_gridContainer);
        card->setFixedSize(cardSize);

        QVBoxLayout* cardLayout = new QVBoxLayout(card);
        cardLayout->setContentsMargins(0, 0, 0, 0);

        QPushButton* body = new QPushButton(card);
        body->setIcon(face.icon);
        body->setIconSize(face.size);
        // An unaffordable card is greyed and does not respond, but its delete icon
        // stays live (REQ-UI-BLUEPRINT-CARD).
        body->setEnabled(m_library->getCanAfford(i));
        cardLayout->addWidget(body);

        // The cost is painted into the face, so what explains the item it names is a
        // child of its own laid over that band -- placed by hand like the delete icon
        // below (REQ-UI-ITEM-VALUE-TOOLTIP). The face is centered in the button, which
        // is what the offset accounts for. A child of the body rather than a sibling, so
        // that the press it does not handle still reaches the button and picks the
        // blueprint; hover only, for the same reason (REQ-UI-TOOLTIP-TRIGGER).
        QWidget* costArea = new QWidget(body);
        const QPoint faceOrigin((cardSize.width()  - face.size.width())  / 2,
                                (cardSize.height() - face.size.height()) / 2);
        costArea->setGeometry(QRect(faceOrigin + face.costRect.topLeft(),
                                    face.costRect.size()));
        ItemTooltip::attachTo(*costArea, m_context, kBlockItemId,
                              TooltipTrigger::Trigger::HoverOnly);

        // A sibling of the body rather than one of its children, and outside the
        // layout: Qt disables a widget's children along with it, and the delete icon
        // must stay enabled on an unaffordable card (REQ-UI-BLUEPRINT-DELETE).
        QPushButton* deleteButton = new QPushButton(QString(kCrossGlyph), card);
        deleteButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
        // Hover only: the click deletes the blueprint (REQ-UI-TOOLTIP-TRIGGER).
        TooltipTrigger::attachText(*deleteButton, tr("Delete blueprint"),
                                   TooltipTrigger::Trigger::HoverOnly);
        deleteButton->move(cardSize.width()  - kSmallButtonSizePx - kCardPaddingPx / 2,
                           cardSize.height() - kSmallButtonSizePx - kCardPaddingPx / 2);
        deleteButton->raise();

        m_grid->addWidget(card, i / kGridColumnCount, i % kGridColumnCount);

        const int index = i;
        connect(body, &QPushButton::clicked, this, [this, index]()
        {
            onCardClicked(index);
        });
        connect(deleteButton, &QPushButton::clicked, this, [this, index]()
        {
            onDeleteClicked(index);
        });
    }
}

void BlueprintSelectionDialog::onCardClicked(int index)
{
    // Placement mode is entered by the caller once the dialog has closed
    // (REQ-UI-BLUEPRINT-CARD).
    m_chosenIndex = index;
    accept();
}

void BlueprintSelectionDialog::onDeleteClicked(int index)
{
    // No confirmation prompt, and the dialog stays open (REQ-UI-BLUEPRINT-DELETE).
    m_library->remove(index);
    rebuildGrid();
}
