<?php

namespace sammo;

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

include "lib.php";
include "func.php";

WebUtil::requireAJAX();

$session = Session::requireLogin([])->setReadOnly();
$userID = Session::getUserID();

increaseRefresh("시뮬레이터", 0);

$query = Util::getPost('query');
if ($query === null) {
    Json::die([
        'result' => false,
        'reason' => '입력값이 없습니다.'
    ]);
}

$action = Util::getPost('action');
if ($action === null || !in_array($action, ['reorder', 'battle'])) {
    Json::die([
        'result' => false,
        'reason' => '원하는 동작이 지정되지 않았습니다.'
    ]);
}

$query = Json::decode($query);
if ($query === null) {
    Json::die([
        'result' => false,
        'reason' => '올바르지 않은 JSON입니다.'
    ]);
}

$defaultCheck = [
    'required' => [
        'attackerGeneral', 'attackerCity', 'attackerNation',
        'defenderGenerals', 'defenderCity', 'defenderNation',
        'year', 'month', 'repeatCnt'
    ],
    'integer' => [
        'year', 'month', 'repeatCnt'
    ],
    'between' => [
        ['month', [1, 12]]
    ],
    'in' => [
        ['repeatCnt', [1, 1000]]
    ],
    'min' => [
        ['year', 0]
    ],
    'array' => [
        'attackerGeneral', 'attackerCity', 'attackerNation',
        'defenderGenerals', 'defenderCity', 'defenderNation'
    ],
];

$v = new Validator($query);
$v->rules($defaultCheck);
if (!$v->validate()) {
    Json::die([
        'result' => false,
        'reason' => $v->errorStr()
    ]);
}

$year = $query['year'];
$month = $query['month'];
$repeatCnt = $query['repeatCnt'];

$rawAttacker = $query['attackerGeneral'];
$battleClock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
$rawAttacker['turntime'] = $battleClock->formatTick($battleClock->nowTick());
$rawAttackerCity = $query['attackerCity'];
$rawAttackerNation = $query['attackerNation'];

$rawDefenderList = $query['defenderGenerals'];
$rawDefenderCity = $query['defenderCity'];
$rawDefenderNation = $query['defenderNation'];

$warSeed = $query['seed'] ?? '';

$cityCheck = [
    'required' => [
        'city', 'nation', 'supply', 'name',
        'pop', 'agri', 'comm', 'secu', 'def', 'wall',
        'trust', 'level',
        'pop_max', 'agri_max', 'comm_max', 'secu_max', 'def_max', 'wall_max',
        'dead', 'state', 'conflict',
    ],
    'numeric' => [
        'pop', 'agri', 'comm', 'secu', 'def', 'wall', 'trust', 'dead'
    ],
    'integer' => [
        'city', 'nation', 'supply',
        'pop_max', 'agri_max', 'comm_max', 'secu_max', 'def_max', 'wall_max',
        'state',
    ],
    'min' => [
        ['def', 0],
        ['wall', 0],
        ['trust', 0],
        ['pop', 0],
        ['comm', 0],
        ['secu', 0],
        ['city', 1],
        ['nation', 0]
    ],
    'in' => [
        ['level', array_keys(getCityLevelList())]
    ]
];

$v = new Validator($rawAttackerCity);
$v->rules($cityCheck);
if (!$v->validate()) {
    Json::die([
        'result' => false,
        'reason' => '[출병도시]' . $v->errorStr()
    ]);
}

$v = new Validator($rawDefenderCity);
$v->rules($cityCheck);
if (!$v->validate()) {
    Json::die([
        'result' => false,
        'reason' => '[수비도시]' . $v->errorStr()
    ]);
}

$nationCheck = [
    'required' => [
        'type', 'tech', 'level', 'capital',
        'nation', 'name', 'gold', 'rice', 'gennum'
    ],
    'integer' => [
        'level', 'capital', 'nation', 'gennum',
    ],
    'numeric' => [
        'tech', 'gold', 'rice'
    ],
    'min' => [
        ['tech', 0],
        ['gold', 0],
        ['rice', 0],
        ['gennum', 1],
    ],
    'in' => [
        ['type', GameConst::$availableNationType],
        ['level', array_keys(getNationLevelList())]
    ]
];

$v = new Validator($rawAttackerNation);
$v->rules($nationCheck);
if (!$v->validate()) {
    Json::die([
        'result' => false,
        'reason' => '[출병국]' . $v->errorStr()
    ]);
}

$v = new Validator($rawDefenderNation);
$v->rules($nationCheck);
if (!$v->validate()) {
    Json::die([
        'result' => false,
        'reason' => '[수비국]' . $v->errorStr()
    ]);
}

$generalCheck = [
    'required' => [
        'no', 'name', 'nation', 'turntime', 'personal', 'special2', 'crew', 'crewtype', 'atmos', 'train',
        'intel', 'intel_exp', 'book', 'strength', 'strength_exp', 'weapon', 'injury', 'leadership', 'leadership_exp', 'horse', 'item',
        'explevel', 'experience', 'dedication', 'officer_level', 'officer_city', 'gold', 'rice', 'dex1', 'dex2', 'dex3', 'dex4', 'dex5',
        'recent_war', 'warnum', 'killnum', 'killcrew',
    ],
    'integer' => [
        'no', 'nation', 'crew', 'crewtype', 'atmos', 'train',
        'intel', 'intel_exp', 'strength', 'strength_exp',  'injury', 'leadership', 'leadership_exp',
        'explevel', 'experience', 'dedication', 'officer_level', 'officer_city', 'gold', 'rice', 'dex1', 'dex2', 'dex3', 'dex4', 'dex5',
        'warnum', 'killnum', 'killcrew',
    ],
    'min' => [
        ['no', 1],
        ['nation', 1],
        ['crew', 0],
        ['intel', 0],
        ['strength', 0],
        ['leadership', 0],
        ['experience', 0],
        ['gold', 0],
        ['rice', 0],
        ['dex1', 0],
        ['dex2', 0],
        ['dex3', 0],
        ['dex4', 0],
        ['dex5', 0],
        ['warnum', 0],
        ['killnum', 0],
        ['killcrew', 0],
    ],
    'between' => [
        ['train', [40, GameConst::$maxTrainByWar]],
        ['atmos', [40, GameConst::$maxAtmosByWar]],
        ['explevel', [0, 300]],
        ['injury', [0, 80]],
        ['officer_level', [1, 12]]
    ],
    'in' => [
        ['personal', array_merge(GameConst::$availablePersonality, GameConst::$optionalPersonality)],
        ['special2', array_merge(GameConst::$availableSpecialWar, GameConst::$optionalSpecialWar)],
        ['crewtype', array_keys(GameUnitConst::all())],
        ['horse', array_merge(array_keys(GameConst::$allItems['horse']), ['None'])],
        ['weapon', array_merge(array_keys(GameConst::$allItems['weapon']), ['None'])],
        ['book', array_merge(array_keys(GameConst::$allItems['book']), ['None'])],
        ['item', array_merge(array_keys(GameConst::$allItems['item']), ['None'])],
    ],
    'array' => ['inheritBuff'],
];

$inheritBuffCheck = [
    'between' => [
        [TriggerInheritBuff::WAR_AVOID_RATIO, [0, 5]],
        [TriggerInheritBuff::WAR_CRITICAL_RATIO, [0, 5]],
        [TriggerInheritBuff::WAR_MAGIC_TRIAL_PROB, [0, 5]],

        [TriggerInheritBuff::OPPOSE_WAR_AVOID_RATIO, [0, 5]],
        [TriggerInheritBuff::OPPOSE_WAR_CRITICAL_RATIO, [0, 5]],
        [TriggerInheritBuff::OPPOSE_WAR_MAGIC_TRIAL_PROB, [0, 5]],
    ]
];

if(!$warSeed){
    $warSeed = bin2hex(random_bytes(16));
}
else {
    $repeatCnt = 1;
}
$tmpRNG = new RandUtil(new LiteHashDRBG($warSeed));

$v = new Validator($rawAttacker);
$v->rules($generalCheck);
if (!$v->validate()) {
    Json::die([
        'result' => false,
        'reason' => '[출병자]' . $v->errorStr()
    ]);
}
$rawAttacker['aux'] = [];
if (key_exists('inheritBuff', $rawAttacker)) {
    $v = new Validator($rawAttacker['inheritBuff']);
    $v->rules($inheritBuffCheck);
    if (!$v->validate()) {
        Json::die([
            'result' => false,
            'reason' => '[출병자]' . $v->errorStr()
        ]);
    }
    $rawAttacker['aux']['inheritBuff'] = $rawAttacker['inheritBuff'];
}
$rawAttacker['aux'] = Json::encode($rawAttacker['aux']);
$rawAttacker['owner'] = 0;
$attackerGeneral = new General($rawAttacker, extractRankVar($rawAttacker), null, $rawAttackerCity, $rawAttackerNation, $year, $month);
$attacker = new WarUnitGeneral(
    $tmpRNG,
    $attackerGeneral,
    $rawAttackerNation,
    true
);


/** @var WarUnit[] */
$defenderList = [];

foreach ($rawDefenderList as $idx => $rawDefenderGeneral) {
    $v = new Validator($rawDefenderGeneral);
    $v->rules($generalCheck);
    if (!$v->validate()) {
        $idx += 1;
        Json::die([
            'result' => false,
            'reason' => "[수비자{$idx}]" . $v->errorStr()
        ]);
    }
    $rawDefenderGeneral['aux'] = [];
    if (key_exists('inheritBuff', $rawDefenderGeneral)) {
        $v = new Validator($rawDefenderGeneral['inheritBuff']);
        $v->rules($inheritBuffCheck);
        if (!$v->validate()) {
            $idx += 1;
            Json::die([
                'result' => false,
                'reason' => "[수비자{$idx}]" . $v->errorStr()
            ]);
        }
        $rawDefenderGeneral['aux']['inheritBuff'] = $rawDefenderGeneral['inheritBuff'];
    }
    $rawDefenderGeneral['aux'] = Json::encode($rawDefenderGeneral['aux']);
    $rawDefenderGeneral['owner'] = 0;

    $defenderGeneral = new General($rawDefenderGeneral, extractRankVar($rawDefenderGeneral), null, $rawDefenderCity, $rawAttackerNation, $year, $month, true);

    $defenderList[] = new WarUnitGeneral(
        $tmpRNG,
        $defenderGeneral,
        $rawDefenderNation,
        false
    );
}

if ($action == 'reorder') {
    $noRng = NoRNG::rngInstance();

    usort($defenderList, function (WarUnit $lhs, WarUnit $rhs) use ($attacker) {
        return - (extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker));
    });

    $order = [];
    foreach ($defenderList as $defender) {
        $order[] = $defender->getGeneral()->getID();
    }

    Json::die([
        'result' => true,
        'reason' => 'success',
        'order' => $order
    ]);
}

$rawDefenderList = [];
foreach($defenderList as $unit){
    if(extractBattleOrder($unit, $attacker) <= 0){
        continue;
    }
    $rawDefenderList[] = $unit->getRaw();
}
unset($defenderList);

$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$startYear = $gameStor->startyear;

function extractRankVar(array $rawVal): Map
{
    $rankVars = new Map;
    foreach ($rawVal as $rawKey => $rawVal) {
        $key = RankColumn::tryFrom($rawKey);
        if ($key === null) {
            continue;
        }
        $rankVars[$key] = $rawVal;
    }
    return $rankVars;
}

function simulateBattle(
    $rawAttacker,
    $rawAttackerCity,
    $rawAttackerNation,
    $rawDefenderList,
    $rawDefenderCity,
    $rawDefenderNation,
    $startYear,
    $year,
    $month,
    $warSeed
) {

    if (!$warSeed) {
        $warSeed = bin2hex(random_bytes(16));
    }

    $warRng = new RandUtil(new LiteHashDRBG($warSeed));

    $attackerGeneral = new General($rawAttacker, extractRankVar($rawAttacker), null, $rawAttackerCity, $rawAttackerNation, $year, $month);
    $attacker = new WarUnitGeneral(
        $warRng,
        $attackerGeneral,
        $rawAttackerNation,
        true
    );
    $city = new WarUnitCity($warRng, $rawDefenderCity, $rawDefenderNation, $year, $month, $startYear);

    /** @var WarUnit[] */
    $defenderList = [];

    foreach ($rawDefenderList as $rawDefenderGeneral) {
        $defenderGeneral = new General($rawDefenderGeneral, extractRankVar($rawDefenderGeneral), null, $rawDefenderCity, $rawAttackerNation, $year, $month, true);

        $defenderList[] = new WarUnitGeneral(
            $warRng,
            $defenderGeneral,
            $rawDefenderNation,
            false
        );
    }

    if(count($defenderList) && extractBattleOrder($city, $attacker) > 0){
        $defenderList[] = $city;
    }

    usort($defenderList, function (WarUnit $lhs, WarUnit $rhs) use ($attacker) {
        return - (extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker));
    });

    $iterDefender = new \ArrayIterator($defenderList);
    $iterDefender->rewind();

    $battleResult = [];

    $attackerRice = $attacker->getGeneral()->getVar('rice');
    $defenderRice = 0;

    $getNextDefender = function (?WarUnit $prevDefender, bool $reqNext)
    use ($iterDefender, &$battleResult, &$defenderRice, $attacker): ?WarUnit {
        if ($prevDefender !== null) {
            $prevDefender->getLogger()->rollback();
            $battleResult[] = $prevDefender;
            if ($prevDefender instanceof WarUnitGeneral) {
                $defenderRice -= $prevDefender->getVar('rice');
            }
        }

        if (!$reqNext) {
            return null;
        }

        if (!$iterDefender->valid()) {
            return null;
        }

        /** @var WarUnit */
        $defender = $iterDefender->current();

        if (extractBattleOrder($defender, $attacker) <= 0) {
            return null;
        }

        if($defender instanceof WarUnitGeneral){
            $defenderRice += $defender->getVar('rice');
        }

        $iterDefender->next();
        return $defender;
    };

    $conquerCity = processWar_NG($warSeed, $attacker, $getNextDefender, $city);

    $attackerRice -= $attacker->getVar('rice');

    if ($city->getPhase() > 0) {
        $rice = $city->getKilled() / 100 * 0.8;
        $rice *= $city->getCrewType()->rice;
        $rice *= getTechCost($city->getRawNation()['tech']);
        $rice *= $city->getCityTrainAtmos() / 100 - 0.2;
        Util::setRound($rice);

        $defenderRice += $rice;
    }

    $totalDead = $attacker->getKilled() + $attacker->getDead();
    //$attackerCityDead = $totalDead * 0.4;
    //$defenderCityDead = $totalDead * 0.6;

    return [$attacker, $city, $battleResult, $conquerCity, $attackerRice, $defenderRice];
}

$lastWarLog = [];

$attackerKilled = 0;
$attackerDead = 0;

$attackerMaxKilled = 0;
$attackerMinKilled = PHP_INT_MAX;

$attackerMaxDead = 0;
$attackerMinDead = PHP_INT_MAX;


$attackerAvgRice = 0;
$defenderAvgRice = 0;

$avgPhase = 0;
$avgWar = 0;

$attackerActivatedSkills = [];
$defendersActivatedSkills = [];


foreach (Util::range($repeatCnt) as $repeatIdx) {
    if($repeatIdx > 0){
        $warSeed = bin2hex(random_bytes(16));
    }

    /** @var WarUnit $attacker */
    [$attacker, $city, $battleResult, $conquerCity, $attackerRice, $defenderRice] = simulateBattle(
        $rawAttacker,
        $rawAttackerCity,
        $rawAttackerNation,
        $rawDefenderList,
        $rawDefenderCity,
        $rawDefenderNation,
        $startYear,
        $year,
        $month,
        $warSeed
    );
    $lastWarLog = Util::mapWithKey(function ($key, $values) {
        return ConvertLog(join('<br>', $values));
    }, $attacker->getLogger()->rollback());

    $avgPhase += $attacker->getPhase() / $repeatCnt;

    $killed = $attacker->getKilled();
    $dead = $attacker->getDead();

    $attackerKilled += $killed / $repeatCnt;
    $attackerDead += $dead / $repeatCnt;

    $attackerMaxKilled = max($attackerMaxKilled, $killed);
    $attackerMinKilled = min($attackerMinKilled, $killed);

    $attackerMaxDead = max($attackerMaxDead, $dead);
    $attackerMinDead = min($attackerMinDead, $dead);

    $attackerAvgRice += $attackerRice / $repeatCnt;
    $defenderAvgRice += $defenderRice / $repeatCnt;

    $avgWar += count($battleResult) / $repeatCnt;

    foreach ($attacker->getActivatedSkillLog() as $skillName => $skillCnt) {
        if (!key_exists($skillName, $attackerActivatedSkills)) {
            $attackerActivatedSkills[$skillName] = $skillCnt / $repeatCnt;
        } else {
            $attackerActivatedSkills[$skillName] += $skillCnt / $repeatCnt;
        }
    }

    foreach ($battleResult as $idx => $defender) {
        while ($idx >= count($defendersActivatedSkills)) {
            $defendersActivatedSkills[] = [];
        }

        $activatedSkills = &$defendersActivatedSkills[$idx];
        foreach ($defender->getActivatedSkillLog() as $skillName => $skillCnt) {
            if (!key_exists($skillName, $activatedSkills)) {
                $activatedSkills[$skillName] = $skillCnt / $repeatCnt;
            } else {
                $activatedSkills[$skillName] += $skillCnt / $repeatCnt;
            }
        }
    }
}

Json::die([
    'result' => true,
    'datetime' => $rawAttacker['turntime'],
    'reason' => 'success',
    'lastWarLog' => $lastWarLog,
    'avgWar' => $avgWar,
    'phase' => $avgPhase,
    'killed' => $attackerKilled,
    'maxKilled' => $attackerMaxKilled,
    'minKilled' => $attackerMinKilled,
    'dead' => $attackerDead,
    'maxDead' => $attackerMaxDead,
    'minDead' => $attackerMinDead,
    'attackerRice' => $attackerAvgRice,
    'defenderRice' => $defenderAvgRice,
    'attackerSkills' => $attackerActivatedSkills,
    'defendersSkills' => $defendersActivatedSkills,
]);
