#include "SelectionPanel.h"

#include <QScrollArea>
#include <QScrollBar>
#include <QVBoxLayout>

#include "BuildingIconCache.h"
#include "ItemIconCache.h"
#include "Simulation.h"
#include "VisualsConfig.h"
#include "selection/SelectionContent.h"

namespace
{

// Distance kept between the panel and the edges of the band it is anchored to
// (REQ-UI-SELECTION-PANEL).
const int kMarginPx = 8;

// Upper bound on the card width. The panel is content-sized, but several of the cards'
// widgets have no natural width of their own -- the word-wrapped summary labels grow
// without limit, and a QListWidget asks for 256 px whatever it holds -- so the width is
// capped and the labels wrap at the cap. 320 px is the width the former side panel
// column had at the default window size.
const int kMaxContentWidthPx = 320;

// Padding between the panel's border and the card inside it.
const int kCardMarginPx = 8;

}  // namespace


SelectionPanel::SelectionPanel(Simulation* sim, const GameConfig* config,
                               const VisualsConfig* visuals, ItemIconCache* itemIcons,
                               BuildingIconCache* buildingIcons, QWidget* parent)
    : QWidget(parent)
{
    m_context.sim              = sim;
    m_context.config           = config;
    m_context.visuals          = visuals;
    m_context.itemIcons        = itemIcons;
    m_context.buildingIcons    = buildingIcons;
    m_context.debugDrawEnabled = &m_debugDrawEnabled;

    // The panel floats over the rendered world rather than sitting in a column, so it
    // brings its own opaque background to stay legible over any world content
    // (REQ-UI-SELECTION-PANEL). Palette colors match the build button bar's chrome; like
    // it, 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 itself
    // rather than cascading onto its child widgets.
    setAttribute(Qt::WA_StyledBackground, true);
    setStyleSheet(QStringLiteral(
        "SelectionPanel { background-color: palette(window);"
        " border: 1px solid palette(mid); border-radius: 4px; }"));

    // A card taller than the band scrolls rather than overrunning it
    // (REQ-UI-SELECTION-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_body = new QWidget(this);
    m_scrollArea = new QScrollArea(this);
    m_scrollArea->setFrameShape(QFrame::NoFrame);
    m_scrollArea->setWidgetResizable(true);
    m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    // The panel works out for itself whether the card fits the band, and sizes itself to
    // leave room for the bar when it does not (REQ-UI-SELECTION-PANEL), so refit() sets
    // this policy rather than leaving the scroll area to decide. Asked to decide, it
    // shows a bar the moment the card is momentarily larger than the viewport -- which
    // happens while the card is being measured -- and does not take it back when the
    // range turns out to be empty.
    m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    m_scrollArea->viewport()->setAutoFillBackground(false);
    m_body->setAutoFillBackground(false);
    m_scrollArea->setWidget(m_body);

    QVBoxLayout* outerLayout = new QVBoxLayout(this);
    outerLayout->setContentsMargins(0, 0, 0, 0);
    outerLayout->setSpacing(0);
    outerLayout->addWidget(m_scrollArea);

    m_bodyLayout = new QVBoxLayout(m_body);
    m_bodyLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
                                     kCardMarginPx, kCardMarginPx);
    m_bodyLayout->setSpacing(0);
    m_bodyLayout->setAlignment(Qt::AlignTop);

    hide();

    registerForEvents();
}

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

void SelectionPanel::anchorTo(const QRect& bandRect)
{
    m_bandRect = bandRect;
    updateVisibility();
}

void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
{
    m_request.buildings = event->ids;
    if (!m_request.buildings.empty())
    {
        // A building selection is exclusive: it supersedes any field selection -- actors
        // and scrap alike (REQ-UI-SELECTION-CATEGORIES).
        m_request.actors.clear();
        m_request.debris.clear();
    }
    rebuildContent();
}

void SelectionPanel::handleEvent(
    std::shared_ptr<const EntitySelectionChangedEvent> event)
{
    m_request.actors = event->entities;
    if (!m_request.actors.empty() || !m_request.debris.empty())
    {
        m_request.buildings.clear();
    }
    rebuildContent();
}

void SelectionPanel::handleEvent(
    std::shared_ptr<const DebrisSelectionChangedEvent> event)
{
    // Debris is a field object: it supersedes any building selection but coexists with
    // actors (REQ-UI-SELECTION-CATEGORIES).
    m_request.debris = event->debris;
    if (!m_request.actors.empty() || !m_request.debris.empty())
    {
        m_request.buildings.clear();
    }
    rebuildContent();
}

void SelectionPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
{
    refreshContent();
}

void SelectionPanel::handleEvent(
    std::shared_ptr<const PlayerCommandsAppliedEvent> /*event*/)
{
    // Player commands (choosing a shipyard schematic, say) are applied by a queued
    // drain, not synchronously. When the game is paused no tick advances, so
    // TickAdvancedEvent never fires; refreshing here too is what makes the change show
    // up without waiting for a tick or a re-selection.
    refreshContent();
}

void SelectionPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
{
    m_debugDrawEnabled = event->active;
}

void SelectionPanel::refreshContent()
{
    if (!m_content)
    {
        return;
    }

    // A card never changes its own shape, so when the selection now calls for a
    // different one it is replaced rather than refreshed. Only a single selected
    // building can reach that state without the selection itself changing -- its
    // construction site finishes, or it is deconstructed under the panel. Everything
    // else is re-published as a selection change, so re-deriving the key here would walk
    // a large multi-selection every tick to learn nothing.
    if (m_request.buildings.size() == 1
        && chooseContent(m_request, *m_context.sim) != m_contentKey)
    {
        rebuildContent();
        return;
    }

    m_content->refresh();
    updateVisibility();
}

void SelectionPanel::rebuildContent()
{
    if (m_content)
    {
        // Retired rather than deleted: a rebuild can be reached from inside one of the
        // card's own click handlers -- the recipe control opens a modal dialog and the
        // choice comes back as a command -- and control has to be able to return into
        // the widget that is going away.
        m_content->hide();
        m_content->deleteLater();
        m_content = nullptr;
    }

    m_contentKey = chooseContent(m_request, *m_context.sim);
    m_content = createContent(m_contentKey, m_request, m_context, m_body);
    if (m_content)
    {
        m_bodyLayout->addWidget(m_content);
        // The show is what makes the card count. A widget created under an
        // already-visible parent starts hidden, and a layout treats a hidden item as
        // empty -- it adds nothing to the size hint until something shows it, which
        // otherwise does not happen until the event loop next runs, long after refit()
        // has measured the panel. The panel then fits itself to an empty body and
        // collapses to its scroll bar.
        m_content->show();
        m_content->refresh();
    }
    updateVisibility();
}

void SelectionPanel::updateVisibility()
{
    // Nothing selected in either category means no panel at all rather than an empty one
    // (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible. Before the
    // owner has anchored the panel there is nowhere to put it either, so it stays hidden
    // until then.
    const bool shouldShow = (m_content != nullptr) && !m_bandRect.isNull();
    setVisible(shouldShow);
    if (shouldShow)
    {
        refit();
    }
}

void SelectionPanel::refit()
{
    const QRect band =
        m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);

    // The panel's border is drawn around the scroll area rather than around the card, so
    // it is added to whatever the card asks for. Spelled out here instead of read back
    // from contentsMargins() because the stylesheet box is what sets it, and asking the
    // style for it before the first show is unreliable.
    const int borderPx    = 1;
    const int maxWidthPx  = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
    const int maxHeightPx = band.height() - 2 * borderPx;
    if (maxWidthPx <= 0 || maxHeightPx <= 0)
    {
        return;
    }

    // What the card asks for at a given width. The width has to be applied before
    // asking, because a card's height depends on the room it is given -- and so, once
    // laid out, does the width it reports. Measuring at whatever width the panel
    // happens to have carries the previous card's shape into this one.
    //
    // Each measurement re-runs the body layout. Cards are built and discarded whole, so
    // its cached hint describes the card before this one until it is invalidated, and
    // re-running it is also what accounts for the parts a card hides and shows as it
    // refreshes. The polish belongs to the same step: a freshly created chip reports an
    // unstyled hint until the stylesheet has reached it, and the chips carry border and
    // padding that change their size.
    auto measureAt = [this](int widthPx) -> QSize
    {
        m_body->resize(widthPx, m_body->height());
        m_body->ensurePolished();
        m_body->layout()->invalidate();
        m_body->layout()->activate();
        return m_body->sizeHint();
    };

    // Run twice. Parts of a card report an unstyled size until the style has actually
    // reached them, which for a freshly built card happens during the first round of
    // measuring; the second round then measures a card that is fully laid out and
    // settles on the answer. Without it a card can end up a few pixels short of what it
    // turns out to need, and the difference shows as a scroll bar over a card that
    // looks like it fits.
    for (int pass = 0; pass < 2; ++pass)
    {
        // First at the cap, the most room the card can ever get, to learn how wide it
        // wants to be; then at that width for the height that follows from it.
        int contentWidthPx  = qMin(measureAt(maxWidthPx).width(), maxWidthPx);
        int contentHeightPx = measureAt(contentWidthPx).height();

        // A card taller than the band is capped there and scrolls
        // (REQ-UI-SELECTION-PANEL). The bar is laid out beside the card, so the panel
        // widens by its width to leave the card the width its height was measured for --
        // and where the cap does not allow that, the card is measured again at what is
        // left over.
        const bool scrolls = (contentHeightPx > maxHeightPx);
        int viewportWidthPx = contentWidthPx;
        if (scrolls)
        {
            const int scrollBarWidthPx =
                m_scrollArea->verticalScrollBar()->sizeHint().width();
            contentWidthPx  = qMin(contentWidthPx + scrollBarWidthPx, maxWidthPx);
            contentHeightPx = maxHeightPx;
            viewportWidthPx = contentWidthPx - scrollBarWidthPx;
            measureAt(viewportWidthPx);
        }
        m_scrollArea->setVerticalScrollBarPolicy(
            scrolls ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);

        // Left at the size the scroll area is about to give it, so the card is not
        // briefly wider than its viewport.
        m_body->resize(viewportWidthPx, m_body->sizeHint().height());

        const int panelWidthPx  = contentWidthPx + 2 * borderPx;
        const int panelHeightPx = contentHeightPx + 2 * borderPx;

        // Right-aligned in the band and centered on it vertically. The band excludes the
        // build button bar's strip, so centering here never puts the panel over the bar
        // (REQ-UI-SELECTION-PANEL).
        setGeometry(band.right() - panelWidthPx + 1,
                    band.top() + (band.height() - panelHeightPx) / 2,
                    panelWidthPx, panelHeightPx);
    }
}
