#include "SelectionBounds.h"

#include "Building.h"
#include "DebrisComponent.h"
#include "EntityAdmin.h"
#include "FactoryQueries.h"
#include "PositionComponent.h"
#include "Simulation.h"
#include "StationBodyComponent.h"
#include "WorldCoordinates.h"
#include "WorldPrimitives.h"

namespace
{

// The widget rectangle of a footprint anchored at a tile, both kinds of body being
// described the same way.
QRectF getFootprintWidgetRect(const WorldCoordinates& coordinates, QPoint anchor,
                              QSize footprint)
{
    const QPointF topLeft = coordinates.tileToWidget(anchor);
    return QRectF(topLeft.x(), topLeft.y(),
                  footprint.width()  * static_cast<qreal>(coordinates.getTilePx()),
                  footprint.height() * static_cast<qreal>(coordinates.getTilePx()));
}

}  // namespace


std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
                                            const WorldCoordinates& coordinates,
                                            BuildingId id)
{
    if (const Building* building = findBuilding(state, id))
    {
        return getFootprintWidgetRect(coordinates, building->anchor, building->footprint);
    }
    if (const ConstructionSite* site = findSite(state, id))
    {
        return getFootprintWidgetRect(coordinates, site->anchor, site->footprint);
    }
    return std::nullopt;
}

std::optional<QRectF> getActorWidgetRect(const EntityAdmin& admin,
                                         const WorldCoordinates& coordinates,
                                         entt::entity actor)
{
    if (!admin.isValid(actor))
    {
        return std::nullopt;
    }
    if (admin.hasAll<StationBodyComponent>(actor))
    {
        const StationBodyComponent& body = admin.get<StationBodyComponent>(actor);
        return getFootprintWidgetRect(coordinates, body.anchor, body.footprint);
    }
    if (admin.hasAll<PositionComponent>(actor))
    {
        // A ship is a triangle about its center; the square its longest extent fits in
        // is what the panel is placed beside.
        const QPointF center =
            coordinates.worldToWidget(admin.get<PositionComponent>(actor).value);
        const qreal extent = static_cast<qreal>(getShipForwardExtentPx(coordinates));
        return QRectF(center.x() - extent, center.y() - extent,
                      2.0 * extent, 2.0 * extent);
    }
    return std::nullopt;
}

std::optional<QRectF> getDebrisWidgetRect(const EntityAdmin& admin,
                                          const WorldCoordinates& coordinates,
                                          entt::entity debris)
{
    if (!admin.isValid(debris) || !admin.hasAll<PositionComponent, DebrisComponent>(debris))
    {
        return std::nullopt;
    }
    const QPointF center =
        coordinates.worldToWidget(admin.get<PositionComponent>(debris).value);
    const qreal radius = static_cast<qreal>(getDebrisRadiusPx(coordinates));
    return QRectF(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius);
}

QRect getSelectionWidgetRect(const Simulation& sim, const WorldCoordinates& coordinates,
                             const std::vector<BuildingId>& buildings,
                             const std::vector<entt::entity>& actors,
                             const std::vector<entt::entity>& debris)
{
    QRectF bounds;
    // A null rect unites to nothing of its own, so the first object found sets the box
    // and every later one grows it.
    auto add = [&bounds](const std::optional<QRectF>& rect)
    {
        if (rect.has_value())
        {
            bounds = bounds.isNull() ? *rect : bounds.united(*rect);
        }
    };

    for (BuildingId id : buildings)
    {
        add(getBuildingWidgetRect(sim.getFactoryState(), coordinates, id));
    }
    for (entt::entity actor : actors)
    {
        add(getActorWidgetRect(sim.getAdmin(), coordinates, actor));
    }
    for (entt::entity piece : debris)
    {
        add(getDebrisWidgetRect(sim.getAdmin(), coordinates, piece));
    }

    return bounds.isNull() ? QRect() : bounds.toAlignedRect();
}
