#include "ControlsPanel.h"

#include <QFont>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QVBoxLayout>

#include "ControlActionText.h"
#include "EventManager.h"
#include "FloatingLayoutInvalidatedEvent.h"
#include "FloatingPanelPlacement.h"
#include "GameWorldView.h"
#include "selection/SelectionNames.h"

namespace
{

const int kMarginPx      = 8;   // between the view's edge and the panel
const int kCardMarginPx  = 8;   // inside the panel, around its content
const int kHeadingGapPx  = 4;   // between the heading and the rows
const int kRefreshMs     = 50;  // see the class comment on why this polls

// The panel's border, from the stylesheet below. Spelled out because the stylesheet box
// is what sets it and asking the style for it before the first show is unreliable.
const int kBorderPx = 1;

// Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler".
const QChar kHeadingSeparator(0x00B7);  // U+00B7 MIDDLE DOT

// The upper-case heading and caption are tracked out a little so they read as labels
// rather than as words. Set on the font because Qt's stylesheets have no letter-spacing
// property -- writing one there is silently ignored apart from a warning per widget.
QFont makeSpacedFont(QFont font, bool bold, int pointSizeDelta)
{
    font.setBold(bold);
    if (pointSizeDelta != 0 && font.pointSize() > 0)
    {
        font.setPointSize(qMax(1, font.pointSize() + pointSizeDelta));
    }
    font.setLetterSpacing(QFont::AbsoluteSpacing, 1.0);
    return font;
}

// One row: the chips for the bindings, then what the action is called. Built as a plain
// widget rather than a class of its own -- it holds no state and answers no questions.
QWidget* makeRow(ControlAction action, const ControlContext& context, QWidget* parent)
{
    QWidget*     row    = new QWidget(parent);
    QHBoxLayout* layout = new QHBoxLayout(row);
    layout->setContentsMargins(0, 2, 0, 2);
    layout->setSpacing(4);

    // Every badge is rendered from the binding the resolver matches, so a chip cannot
    // claim a key that does nothing (REQ-UI-CONTROLS-ACCURACY). The chips of the row
    // that leaves the context are the ones marked, not its label (REQ-UI-CONTROLS-CARD):
    // leaving a build mode and clearing the selection are the same gesture to the player,
    // one key that hands the context back, so they are marked alike.
    const bool backsOut = (action == ControlAction::ExitMode
                           || action == ControlAction::ClearSelection);
    const std::vector<ControlBinding> bindings = getControlActionBindings(action, context);
    for (const ControlBinding& binding : bindings)
    {
        QLabel* badge = new QLabel(getControlBindingBadge(binding), row);
        badge->setObjectName(backsOut ? QStringLiteral("controlBadgeExit")
                                      : QStringLiteral("controlBadge"));
        layout->addWidget(badge);
    }

    QLabel* label = new QLabel(getControlActionLabel(action, context), row);
    label->setObjectName(QStringLiteral("controlLabel"));
    layout->addSpacing(4);
    layout->addWidget(label);
    layout->addStretch(1);
    return row;
}

// Adds a freshly built widget to the rows and shows it.
//
// The show is what makes it count. A widget created under an already-visible parent
// starts hidden, and a layout treats a hidden item as empty -- it contributes nothing
// to the size hint until something shows it, which otherwise does not happen until the
// event loop next runs, long after the panel has measured itself.
void addAndShow(QVBoxLayout* layout, QWidget* widget)
{
    layout->addWidget(widget);
    widget->show();
}

}  // namespace

ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
    : QWidget(parent)
    , m_view(view)
{
    // Floats over the rendered world, so it brings its own background to stay legible
    // over any world content (REQ-UI-CONTROLS-PANEL). Palette colors match the build
    // button bar's and the selection panel's chrome; like them this is widget chrome
    // rather than world rendering, so it is deliberately not a visuals.toml color. The
    // class scoped selector keeps the border on the panel rather than cascading onto
    // its children.
    setAttribute(Qt::WA_StyledBackground, true);
    // Letter spacing is deliberately absent here: Qt's stylesheet syntax has no such
    // property and warns on every widget it is applied to. The heading and the caption
    // set it on their QFont instead.
    setStyleSheet(QStringLiteral(
        "ControlsPanel { background-color: palette(window);"
        " border: 1px solid palette(mid); border-radius: 4px; }"
        "QLabel#controlHeading { color: palette(text); }"
        "QLabel#controlBadge { border: 1px solid palette(mid); border-radius: 3px;"
        " padding: 1px 5px; font-family: monospace; color: palette(text); }"
        "QLabel#controlLabel { color: palette(text); }"
        // The row that leaves the mode is marked on its chips rather than its label
        // (REQ-UI-CONTROLS-CARD): the label keeps the ordinary text color, so the row
        // stays legible whatever the palette, and only the chips carry the warning.
        //
        // A literal red because there is no palette role for "destructive" -- the
        // nearest, bright-text, is white by design, being meant for text over dark
        // highlights, and was unreadable on this panel's chrome. This value reads on a
        // light and a dark background alike. It is widget chrome, so like the rest of
        // this stylesheet it is deliberately not a visuals.toml color.
        "QLabel#controlBadgeExit { border: 1px solid #c0392b; border-radius: 3px;"
        " padding: 1px 5px; font-family: monospace; color: #c0392b; }"
        "QLabel#controlCaption { color: palette(mid); }"));

    QVBoxLayout* outerLayout = new QVBoxLayout(this);
    outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
                                    kCardMarginPx, kCardMarginPx);
    outerLayout->setSpacing(kHeadingGapPx);

    m_heading = new QLabel(this);
    m_heading->setObjectName(QStringLiteral("controlHeading"));
    m_heading->setCursor(Qt::PointingHandCursor);
    m_heading->setFont(makeSpacedFont(font(), /*bold*/ true, /*pointSizeDelta*/ 0));
    outerLayout->addWidget(m_heading);

    // Rows taller than the space available scroll rather than being cut off
    // (REQ-UI-CONTROLS-PANEL). The viewport is transparent so the panel's own rounded
    // chrome shows through, and horizontal scrolling is off because the width always
    // follows the content.
    m_rows       = new QWidget(this);
    m_rowsLayout = new QVBoxLayout(m_rows);
    m_rowsLayout->setContentsMargins(0, 0, 0, 0);
    m_rowsLayout->setSpacing(0);
    m_rows->setAutoFillBackground(false);

    m_scrollArea = new QScrollArea(this);
    m_scrollArea->setFrameShape(QFrame::NoFrame);
    m_scrollArea->setWidgetResizable(true);
    m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    m_scrollArea->viewport()->setAutoFillBackground(false);
    m_scrollArea->setWidget(m_rows);
    outerLayout->addWidget(m_scrollArea);

    // Polling rather than subscribing; see the class comment.
    m_refreshTimer = new QTimer(this);
    connect(m_refreshTimer, &QTimer::timeout, this, &ControlsPanel::refresh);
    m_refreshTimer->start(kRefreshMs);

    refresh();
}

void ControlsPanel::invalidateLayout()
{
    EventManager::getInstance()->sendEventImmediately(
        std::make_shared<FloatingLayoutInvalidatedEvent>());
}

void ControlsPanel::mousePressEvent(QMouseEvent* event)
{
    // Only the heading toggles; a click anywhere else is swallowed so it never reaches
    // the world beneath (REQ-UI-CONTROLS-PANEL).
    if (m_heading->geometry().contains(event->pos()))
    {
        m_collapsed = !m_collapsed;
        m_scrollArea->setVisible(!m_collapsed);
        invalidateLayout();
    }
    event->accept();
}

void ControlsPanel::refresh()
{
    if (!m_view) { return; }

    const ControlContext context = m_view->getControlContext();

    const QString                    heading = getHeadingText(context);
    const std::vector<ControlAction> contextActions = getContextActions(context);
    const std::vector<ControlAction> alwaysActions  = getAlwaysAvailableActions(context);

    if (heading == m_shownHeading && contextActions == m_shownContextActions
        && alwaysActions == m_shownAlwaysActions)
    {
        return;
    }

    m_shownHeading        = heading;
    m_shownContextActions = contextActions;
    m_shownAlwaysActions  = alwaysActions;
    rebuild(context);
}

void ControlsPanel::rebuild(const ControlContext& context)
{
    m_heading->setText(m_shownHeading);

    // Rows are rebuilt wholesale rather than reconciled: a context change replaces
    // nearly all of them, and the panel redraws only when something actually changed.
    while (QLayoutItem* item = m_rowsLayout->takeAt(0))
    {
        delete item->widget();
        delete item;
    }

    for (ControlAction action : m_shownContextActions)
    {
        addAndShow(m_rowsLayout, makeRow(action, context, m_rows));
    }

    // The always-available block sits under a divider in every context, the General one
    // included, so the card is read the same way wherever the player is
    // (REQ-UI-CONTROLS-CARD).
    if (!m_shownAlwaysActions.empty())
    {
        QFrame* divider = new QFrame(m_rows);
        divider->setFrameShape(QFrame::HLine);
        divider->setFrameShadow(QFrame::Plain);
        m_rowsLayout->addSpacing(6);
        addAndShow(m_rowsLayout, divider);

        QLabel* caption = new QLabel(tr("ALWAYS AVAILABLE"), m_rows);
        caption->setObjectName(QStringLiteral("controlCaption"));
        caption->setFont(makeSpacedFont(font(), /*bold*/ false, /*pointSizeDelta*/ -1));
        addAndShow(m_rowsLayout, caption);
    }

    for (ControlAction action : m_shownAlwaysActions)
    {
        addAndShow(m_rowsLayout, makeRow(action, context, m_rows));
    }

    m_scrollArea->setVisible(!m_collapsed);
    invalidateLayout();
}

QString ControlsPanel::getHeadingText(const ControlContext& context) const
{
    const ControlContextKind kind = getControlContextKind(context);
    const QString            name = getControlContextName(kind);

    QString detail;
    switch (kind)
    {
    case ControlContextKind::Build:
        detail = getBuildingTypeName(context.builderType);
        break;
    case ControlContextKind::Blueprint:
    {
        // The temporary blueprint is never named (REQ-UI-BLUEPRINT-TEMP), so it is
        // labelled by what it is rather than left blank.
        const QString blueprintName = m_view->getActiveBlueprintName();
        detail = blueprintName.isEmpty() ? tr("Temporary") : blueprintName;
        break;
    }
    case ControlContextKind::Selection:
        detail = context.selection == ControlSelection::Buildings
                     ? tr("%n building(s)", "", context.selectionCount)
                     : tr("%n object(s)", "", context.selectionCount);
        break;
    case ControlContextKind::General:
    case ControlContextKind::Deconstruct:
        break;
    }

    if (detail.isEmpty()) { return name; }
    return name + QStringLiteral(" ") + kHeadingSeparator + QStringLiteral(" ") + detail;
}

void ControlsPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& occupiedRects)
{
    if (viewRect.isNull()) { return; }

    // Rows are torn down and rebuilt wholesale, and a widget added to a layout is only
    // shown once that layout runs -- without this the new rows count for nothing and
    // the card is measured for the context before it. The polish belongs to the same
    // step: a freshly created label reports an unstyled size hint until the stylesheet
    // has reached it, and the badge chips carry border and padding that change it.
    m_rows->ensurePolished();
    m_rowsLayout->invalidate();
    m_rowsLayout->activate();

    const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
    if (band.width() <= 0 || band.height() <= 0) { return; }

    // Measured from the heading and the rows directly rather than from the panel's own
    // layout: the rows now sit in a scroll area, whose size hint describes a viewport
    // and says nothing about how tall its contents are. Deliberately not activating the
    // panel's own layout either -- that lays its children out inside the geometry left
    // over from the previous context, which is the wrong frame of reference for
    // choosing the new one. setGeometry below re-runs it.
    const int   chromePx    = 2 * (kCardMarginPx + kBorderPx);
    const QSize headingHint = m_heading->sizeHint();
    const QSize rowsHint    = m_rows->sizeHint();

    int contentWidthPx  = headingHint.width();
    int contentHeightPx = headingHint.height();
    if (!m_collapsed)
    {
        contentWidthPx  = qMax(contentWidthPx, rowsHint.width());
        contentHeightPx += kHeadingGapPx + rowsHint.height();
    }
    const int wantedHeightPx = contentHeightPx + chromePx;

    // The bottom-left corner of the view, growing upward as rows are added
    // (REQ-UI-CONTROLS-PANEL).
    int widthPx = qMin(contentWidthPx + chromePx, band.width());

    // The build button bar is centered and sized to its buttons, so it usually leaves
    // this corner free and the panel can share the bottom edge with it. Only where the
    // two would actually meet does the panel rise, clearing the bar's top by the same
    // margin it keeps from the view's edges (REQ-UI-BUILD-BAR). The bar is the only
    // widget placed before this one, so it is the only rect that can be in the way.
    const int bottomPx = getAvailableBottomPx(band, occupiedRects, band.left(),
                                              band.left() + widthPx - 1, kMarginPx);
    int heightPx = qMin(wantedHeightPx, qMax(0, bottomPx - band.top() + 1));
    int topPx    = bottomPx - heightPx + 1;

    // Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
    // own width, or it would appear over the labels.
    if (heightPx < wantedHeightPx)
    {
        widthPx = qMin(widthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(),
                       band.width());
    }

    setGeometry(band.left(), topPx, widthPx, heightPx);
}
