#include "WaveSystem.h"

#include <algorithm>

#include "ShipSystem.h"
#include "ThreatCostCalculator.h"
#include "tracing.h"

WaveSystem::WaveSystem(const GameConfig& config, std::mt19937& rng)
    : m_config(config)
    , m_rng(rng)
{
    m_bossCountdownTicks      = secondsToTicks(config.world.waves.bossCountdownSeconds);
    m_normalGapRemainingTicks = drawGapTicks();
}

void WaveSystem::tickWaveScheduler(Tick currentTick, ShipSystem& ships,
                                    int worldHeightTiles)
{
	TRACE();
    // 1. Advance boss countdown.
    --m_bossCountdownTicks;

    // 2. Trigger boss wave when countdown expires.
    if (m_bossCountdownTicks <= 0)
    {
        triggerBossWave(currentTick, worldHeightTiles);
    }

    // 3. Advance post-boss quiet window.
    if (m_postBossQuietRemainingTicks > 0)
    {
        --m_postBossQuietRemainingTicks;
    }

    // 4. Normal wave gap: only advances outside quiet windows and between waves.
    if (!isInQuietWindow() && !m_normalWaveActive)
    {
        if (m_normalGapRemainingTicks > 0)
        {
            --m_normalGapRemainingTicks;
        }
        if (m_normalGapRemainingTicks == 0)
        {
            triggerNormalWave(currentTick, worldHeightTiles);
        }
    }

    // 5. Spawn any ships (from either queue) whose scheduled tick has arrived.
    auto spawnDue = [&](std::vector<SpawnEntry>& queue)
    {
        std::vector<SpawnEntry> remaining;
        remaining.reserve(queue.size());
        for (const SpawnEntry& entry : queue)
        {
            if (currentTick >= entry.spawnAt)
            {
                ships.spawn(entry.schematicId, entry.position,
                            /*isEnemy=*/true, entry.layout);
            }
            else
            {
                remaining.push_back(entry);
            }
        }
        queue = std::move(remaining);
    };
    spawnDue(m_normalPendingSpawns);
    spawnDue(m_bossPendingSpawns);

    // 6. When all normal wave ships have spawned, draw the next gap.
    if (m_normalWaveActive && m_normalPendingSpawns.empty())
    {
        m_normalWaveActive        = false;
        m_normalGapRemainingTicks = drawGapTicks();
    }
}

void WaveSystem::tickThreatAccumulation()
{
	TRACE();
    if (isInQuietWindow())
    {
        return;
    }
    const double x    = static_cast<double>(m_bossWaveCounter);
    const double rate = m_config.world.waves.threatRateFormula.evaluate(x);
    if (rate > 0.0)
    {
        m_threatLevel += rate * kTickDurationSeconds;
    }
}

void WaveSystem::onEnemyStationsDestroyed()
{
    const Tick advance      = secondsToTicks(m_config.world.push.bossAdvanceSeconds);
    m_bossCountdownTicks    = std::max(Tick{0}, m_bossCountdownTicks - advance);
    ++m_generation;
}

double WaveSystem::getThreatLevel() const
{
    return m_threatLevel;
}

double WaveSystem::getThreatAccumulationRate() const
{
    if (isInQuietWindow())
    {
        return 0.0;
    }
    const double x = static_cast<double>(m_bossWaveCounter);
    return std::max(0.0, m_config.world.waves.threatRateFormula.evaluate(x));
}

int WaveSystem::getGeneration() const
{
    return m_generation;
}

int WaveSystem::getBossWaveCounter() const
{
    return m_bossWaveCounter;
}

Tick WaveSystem::getBossCountdownTicks() const
{
    return m_bossCountdownTicks;
}

Tick WaveSystem::getNormalGapRemainingTicks() const
{
    return m_normalGapRemainingTicks;
}

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

bool WaveSystem::isInQuietWindow() const
{
    if (m_postBossQuietRemainingTicks > 0)
    {
        return true;
    }
    const Tick quietBeforeTicks = secondsToTicks(m_config.world.waves.bossQuietBeforeSeconds);
    return m_bossCountdownTicks <= quietBeforeTicks;
}

void WaveSystem::triggerNormalWave(Tick currentTick, int worldHeightTiles)
{
    double budget = m_threatLevel;
    m_threatLevel = 0.0;
    m_normalPendingSpawns = selectWaveShips(budget, currentTick, worldHeightTiles);
    m_threatLevel += budget;  // carry leftover forward
    m_normalWaveActive = true;
}

void WaveSystem::triggerBossWave(Tick currentTick, int worldHeightTiles)
{
    const double x    = static_cast<double>(m_bossWaveCounter);
    const double rate = std::max(0.0, m_config.world.waves.threatRateFormula.evaluate(x));
    double budget = rate * m_config.world.waves.bossThreatDurationSeconds + m_threatLevel;
    m_threatLevel = 0.0;
    m_bossPendingSpawns = selectWaveShips(budget, currentTick, worldHeightTiles);
    m_threatLevel += budget;  // carry leftover to first normal wave of new cycle

    m_bossCountdownTicks          = secondsToTicks(m_config.world.waves.bossCountdownSeconds);
    m_postBossQuietRemainingTicks = secondsToTicks(m_config.world.waves.bossQuietAfterSeconds);
    ++m_bossWaveCounter;
}

std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
                                                                  Tick currentTick,
                                                                  int worldHeightTiles)
{
    // Build eligible ship list with their (level-independent) threat costs.
    struct EligibleShip
    {
        std::string               schematicId;
        double                    cost;
        std::vector<PlacedModule> defaultModules;
    };
    std::vector<EligibleShip> eligible;
    for (const ShipDef& def : m_config.ships.ships)
    {
        const double cost = calculateShipThreatCost(m_config.threatCosts, m_config,
                                                      def.id, def.defaultModules);
        if (cost > 0.0)
        {
            EligibleShip es;
            es.schematicId    = def.id;
            es.cost           = cost;
            es.defaultModules = def.defaultModules;
            eligible.push_back(es);
        }
    }

    if (eligible.empty())
    {
        return {};
    }

    // Enemy spawn buffer X range for the current generation.
    const float leftX = static_cast<float>(
        m_config.world.regions.playerBufferWidth_tiles
        + m_config.world.regions.contestZoneWidth_tiles
        + m_generation * m_config.world.push.pushExpandColumns_tiles);
    const float rightX = leftX
        + static_cast<float>(m_config.world.regions.enemyBufferWidth_tiles) - 1.0f;

    std::uniform_real_distribution<float> xDist(leftX, rightX);
    std::uniform_int_distribution<int>    yDist(0, worldHeightTiles - 1);

    std::vector<SpawnEntry> picked;

    while (true)
    {
        // Collect indices of ships whose cost fits the remaining budget.
        std::vector<std::size_t> fitting;
        for (std::size_t i = 0; i < eligible.size(); ++i)
        {
            if (eligible[i].cost <= budget)
            {
                fitting.push_back(i);
            }
        }
        if (fitting.empty())
        {
            break;
        }

        std::uniform_int_distribution<int> pick(0, static_cast<int>(fitting.size()) - 1);
        const std::size_t   chosenIdx = fitting[static_cast<std::size_t>(pick(m_rng))];
        const EligibleShip& chosen    = eligible[chosenIdx];

        budget -= chosen.cost;

        SpawnEntry entry;
        entry.schematicId          = chosen.schematicId;
        entry.spawnAt              = 0;  // set below after all picks are done
        entry.position             = QVector2D(xDist(m_rng),
                                               static_cast<float>(yDist(m_rng)) + 0.5f);
        entry.layout.placedModules = chosen.defaultModules;
        picked.push_back(entry);
    }

    // Spread spawn times evenly across spawnDurationSeconds.
    const int count = static_cast<int>(picked.size());
    if (count == 1)
    {
        picked[0].spawnAt = currentTick;
    }
    else if (count > 1)
    {
        const Tick spawnDurationTicks =
            secondsToTicks(m_config.world.waves.spawnDurationSeconds);
        for (int i = 0; i < count; ++i)
        {
            picked[static_cast<std::size_t>(i)].spawnAt =
                currentTick + static_cast<Tick>(i) * spawnDurationTicks / (count - 1);
        }
    }

    return picked;
}

Tick WaveSystem::drawGapTicks()
{
    const Tick minTicks = secondsToTicks(m_config.world.waves.gapMinSeconds);
    const Tick maxTicks = secondsToTicks(m_config.world.waves.gapMaxSeconds);
    std::uniform_int_distribution<Tick> dist(minTicks, maxTicks);
    return dist(m_rng);
}
