<?php

declare(strict_types=1);

namespace sammo;

use Ds\Map;
use sammo\Enums\RankColumn;

if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit;
}

chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';

final class ComparisonTracingRNG implements RNG
{
    private int $sequence = 0;

    /** @var array<int, array<string, mixed>> */
    public array $calls = [];

    public function __construct(private readonly RNG $inner)
    {
    }

    public static function getMaxInt(): int
    {
        return LiteHashDRBG::getMaxInt();
    }

    public function nextBytes(int $bytes): string
    {
        $value = $this->inner->nextBytes($bytes);
        $this->record('nextBytes', ['bytes' => $bytes], bin2hex($value));
        return $value;
    }

    public function nextBits(int $bits): string
    {
        $value = $this->inner->nextBits($bits);
        $this->record('nextBits', ['bits' => $bits], bin2hex($value));
        return $value;
    }

    public function nextInt(?int $max = null): int
    {
        $value = $this->inner->nextInt($max);
        $this->record('nextInt', ['maxInclusive' => $max], $value);
        return $value;
    }

    public function nextFloat1(): float
    {
        $value = $this->inner->nextFloat1();
        $this->record('nextFloat1', [], $value);
        return $value;
    }

    private function record(string $operation, array $arguments, mixed $result): void
    {
        $this->calls[] = [
            'seq' => $this->sequence++,
            'operation' => $operation,
            'arguments' => $arguments,
            'result' => $result,
        ];
    }
}

function comparisonExtractRankVar(array $raw): Map
{
    $rankVars = new Map();
    foreach ($raw as $rawKey => $rawValue) {
        $key = RankColumn::tryFrom($rawKey);
        if ($key !== null) {
            $rankVars[$key] = $rawValue;
        }
    }
    return $rankVars;
}

function comparisonBuildGeneral(
    array $raw,
    array $city,
    array $nation,
    int $year,
    int $month,
    bool $defender = false,
): General {
    $aux = [];
    if (array_key_exists('inheritBuff', $raw)) {
        $aux['inheritBuff'] = $raw['inheritBuff'];
    }
    $raw['aux'] = Json::encode($aux);
    $raw['owner'] = 0;
    return new General(
        $raw,
        comparisonExtractRankVar($raw),
        null,
        $city,
        $nation,
        $year,
        $month,
        $defender,
    );
}

function comparisonRunBattle(array $fixture): array
{
    $seed = (string)($fixture['seed'] ?? 'battle-differential');
    $year = (int)$fixture['year'];
    $month = (int)$fixture['month'];
    $startYear = (int)($fixture['startYear'] ?? 180);
    $rawAttacker = $fixture['attackerGeneral'];
    $rawAttackerCity = $fixture['attackerCity'];
    $rawAttackerNation = $fixture['attackerNation'];
    $rawDefenderCity = $fixture['defenderCity'];
    $rawDefenderNation = $fixture['defenderNation'];

    $tracingRng = new ComparisonTracingRNG(new LiteHashDRBG($seed));
    $warRng = new RandUtil($tracingRng);
    $attacker = new WarUnitGeneral(
        $warRng,
        comparisonBuildGeneral($rawAttacker, $rawAttackerCity, $rawAttackerNation, $year, $month),
        $rawAttackerNation,
        true,
    );
    $city = new WarUnitCity($warRng, $rawDefenderCity, $rawDefenderNation, $year, $month, $startYear);

    $defenderList = [];
    foreach ($fixture['defenderGenerals'] as $rawDefender) {
        $defenderList[] = new WarUnitGeneral(
            $warRng,
            comparisonBuildGeneral($rawDefender, $rawDefenderCity, $rawDefenderNation, $year, $month, true),
            $rawDefenderNation,
            false,
        );
    }
    if (count($defenderList) && extractBattleOrder($city, $attacker) > 0) {
        $defenderList[] = $city;
    }
    usort(
        $defenderList,
        fn(WarUnit $lhs, WarUnit $rhs): int =>
            -(extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker)),
    );

    $iterDefender = new \ArrayIterator($defenderList);
    $iterDefender->rewind();
    $finishedDefenders = [];
    $getNextDefender = function (?WarUnit $previous, bool $requestNext) use (
        $iterDefender,
        $attacker,
        &$finishedDefenders,
    ): ?WarUnit {
        if ($previous !== null) {
            $finishedDefenders[] = buildWarTraceUnitSnapshot($previous);
        }
        if (!$requestNext || !$iterDefender->valid()) {
            return null;
        }
        $next = $iterDefender->current();
        if (extractBattleOrder($next, $attacker) <= 0) {
            return null;
        }
        $iterDefender->next();
        return $next;
    };

    $events = [];
    $conquered = processWar_NG(
        $seed,
        $attacker,
        $getNextDefender,
        $city,
        static function (array $event) use (&$events): void {
            $events[] = $event;
        },
    );

    return [
        'engine' => 'ref',
        'seed' => $seed,
        'conquered' => $conquered,
        'attacker' => buildWarTraceUnitSnapshot($attacker),
        'city' => buildWarTraceUnitSnapshot($city),
        'finishedDefenders' => $finishedDefenders,
        'events' => $events,
        'rng' => $tracingRng->calls,
    ];
}

$fixturePath = $argv[1] ?? null;
if ($fixturePath === null || ($fixturePath !== '-' && !is_file($fixturePath))) {
    fwrite(STDERR, "usage: php compare/battle_trace.php <fixture.json|->\n");
    exit(2);
}

try {
    $fixtureJson = $fixturePath === '-' ? stream_get_contents(STDIN) : file_get_contents($fixturePath);
    $fixture = json_decode((string)$fixtureJson, true, flags: JSON_THROW_ON_ERROR);
    echo json_encode(
        comparisonRunBattle($fixture),
        JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
    ), PHP_EOL;
} catch (\Throwable $throwable) {
    fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
    exit(1);
}
