#include "TunnelCompletion.h"

namespace
{

QPoint stepTile(QPoint tile, Rotation dir)
{
    switch (dir)
    {
        case Rotation::North: return {tile.x(),     tile.y() - 1};
        case Rotation::East:  return {tile.x() + 1, tile.y()    };
        case Rotation::South: return {tile.x(),     tile.y() + 1};
        case Rotation::West:  return {tile.x() - 1, tile.y()    };
    }
    return tile;
}

Rotation oppositeRotation(Rotation dir)
{
    switch (dir)
    {
        case Rotation::North: return Rotation::South;
        case Rotation::East:  return Rotation::West;
        case Rotation::South: return Rotation::North;
        case Rotation::West:  return Rotation::East;
    }
    return dir;
}

float tileCenterDistanceSq(QPoint tile, QVector2D cursorWorldPos)
{
    const QVector2D center(static_cast<float>(tile.x()) + 0.5f,
                           static_cast<float>(tile.y()) + 0.5f);
    return (center - cursorWorldPos).lengthSquared();
}

}  // namespace

std::optional<QPoint> firstTunnelFacing(const TunnelLookup& lookup, QPoint start,
                                        Rotation stepDir, Rotation targetFacing,
                                        int maxDistance)
{
    QPoint probe = start;
    for (int distance = 1; distance <= maxDistance; ++distance)
    {
        probe = stepTile(probe, stepDir);
        const std::optional<TunnelTileInfo> info = lookup(probe);
        if (info.has_value() && info->rotation == targetFacing)
        {
            return probe;
        }
    }
    return std::nullopt;
}

TunnelCompletion resolveTunnelCompletion(const TunnelLookup& lookup, QPoint hoverTile,
                                         Rotation rotation, int maxDistance,
                                         QVector2D cursorWorldPos)
{
    // Entry-completion: a hypothetical entry at the hovered tile searches ahead (its
    // facing direction) for a same-direction exit to pair with. The first
    // same-direction tunnel ahead completes the entry only if it is an exit.
    std::optional<QPoint> exitPartner;
    if (const std::optional<QPoint> ahead =
            firstTunnelFacing(lookup, hoverTile, rotation, rotation, maxDistance);
        ahead.has_value())
    {
        const std::optional<TunnelTileInfo> info = lookup(*ahead);
        if (info.has_value() && info->type == BuildingType::TunnelExit)
        {
            exitPartner = ahead;
        }
    }

    // Exit-completion: a hypothetical exit at the hovered tile pairs with an existing
    // entry behind it — one whose forward search (its facing direction) reaches the
    // hovered tile as its first same-direction tunnel. Scanning backwards, the first
    // same-direction tunnel completes the exit only if it is an entry.
    std::optional<QPoint> entryPartner;
    if (const std::optional<QPoint> behind =
            firstTunnelFacing(lookup, hoverTile, oppositeRotation(rotation), rotation,
                              maxDistance);
        behind.has_value())
    {
        const std::optional<TunnelTileInfo> info = lookup(*behind);
        if (info.has_value() && info->type == BuildingType::TunnelEntry)
        {
            entryPartner = behind;
        }
    }

    // Default: a TunnelEntry with no partner.
    if (!entryPartner.has_value() && !exitPartner.has_value())
    {
        return TunnelCompletion{BuildingType::TunnelEntry, std::nullopt};
    }
    if (entryPartner.has_value() && !exitPartner.has_value())
    {
        return TunnelCompletion{BuildingType::TunnelExit, entryPartner};
    }
    if (exitPartner.has_value() && !entryPartner.has_value())
    {
        return TunnelCompletion{BuildingType::TunnelEntry, exitPartner};
    }

    // Both apply: complete whichever existing partner is closer to the sub-tile
    // cursor position (REQ-BLD-TUNNEL-MODE).
    const float entryDistanceSq = tileCenterDistanceSq(*entryPartner, cursorWorldPos);
    const float exitDistanceSq  = tileCenterDistanceSq(*exitPartner,  cursorWorldPos);
    if (entryDistanceSq <= exitDistanceSq)
    {
        return TunnelCompletion{BuildingType::TunnelExit, entryPartner};
    }
    return TunnelCompletion{BuildingType::TunnelEntry, exitPartner};
}

std::optional<QPoint> findTunnelPartner(const TunnelLookup& lookup, QPoint tile,
                                        BuildingType type, Rotation rotation,
                                        int maxDistance)
{
    if (type == BuildingType::TunnelEntry)
    {
        // An entry pairs with the first same-direction tunnel ahead, if it is an exit.
        const std::optional<QPoint> ahead =
            firstTunnelFacing(lookup, tile, rotation, rotation, maxDistance);
        if (ahead.has_value())
        {
            const std::optional<TunnelTileInfo> info = lookup(*ahead);
            if (info.has_value() && info->type == BuildingType::TunnelExit)
            {
                return ahead;
            }
        }
        return std::nullopt;
    }

    if (type == BuildingType::TunnelExit)
    {
        // An exit is claimed by the first same-direction tunnel behind it, if it is an
        // entry (its forward search reaches this exit as its first same-direction tunnel).
        const std::optional<QPoint> behind =
            firstTunnelFacing(lookup, tile, oppositeRotation(rotation), rotation, maxDistance);
        if (behind.has_value())
        {
            const std::optional<TunnelTileInfo> info = lookup(*behind);
            if (info.has_value() && info->type == BuildingType::TunnelEntry)
            {
                return behind;
            }
        }
        return std::nullopt;
    }

    return std::nullopt;
}

TunnelLookup makeTunnelLookup(const TunnelTileMap& tunnels)
{
    return [&tunnels](QPoint tile) -> std::optional<TunnelTileInfo>
    {
        const TunnelTileMap::const_iterator it = tunnels.find(tile);
        if (it == tunnels.end()) { return std::nullopt; }
        return it->second;
    };
}
