//
// Created by Dan Crosby on 6/3/20.
//

#include "UpdateGameStatusAction.hpp"

#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_status.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/player_info.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/victory_condition.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/game_status.pb.h"

namespace shardok {

using net::eagle0::shardok::storage::fb::VictoryCondition;
using PlayerInfoFb = net::eagle0::shardok::storage::fb::PlayerInfo;
using GameStatusFb = net::eagle0::shardok::storage::fb::GameStatus;
using GameStatusProto = net::eagle0::shardok::common::GameStatus;
using std::optional;
using std::unordered_set;

static inline auto GameIsOver(const GameStatusFb* status) -> bool {
    return (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY);
}

auto UpdateGameStatusAction::GetPlayerInfo(int playerId) const -> const PlayerInfoFb* {
    return *std::find_if(
            std::begin(*gameState->player_infos()),
            std::end(*gameState->player_infos()),
            [playerId](const PlayerInfoFb* pi) { return pi->player_id() == playerId; });
}

static inline auto HasVictoryCondition(const PlayerInfoFb* pi, const VictoryCondition condition)
        -> bool {
    for (const auto& vc : *pi->victory_conditions()) {
        if (vc == condition) return true;
    }
    return false;
}

auto UpdateGameStatusAction::SurvivingPlayers() const -> unordered_set<PlayerId> {
    if (gameState->status()->state() ==
        net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP)
        return unordered_set<PlayerId>{};

    unordered_set<PlayerId> survivingPids{};

    for (const Unit* unit : *gameState->units()) {
        if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;

        if ((uint32_t)unit->player_id() >= gameState->player_infos()->size()) continue;

        if (!unit->hidden() && (unit->battalion().size() > 0 ||
                                (unit->has_attached_hero() && unit->attached_hero().vigor() > 0))) {
            survivingPids.insert(unit->player_id());
        }
    }

    return survivingPids;
}

void UpdateGameStatusAction::ResolveHiddenLosers(ActionResult& actionResult) const {
    const auto& winningIds = actionResult.game_status().winning_shardok_ids();

    for (const auto* unit : *gameState->units()) {
        if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;

        if (!common::Contains(winningIds, unit->player_id()) && unit->hidden() &&
            unit->can_flee() && unit->has_attached_hero() &&
            unit->attached_hero().vigor() >= settingsGetter.Backing().minimum_vigor_to_act()) {
            net::eagle0::shardok::storage::ResolvedUnit* fledRanger =
                    actionResult.add_resolved_units();

            AddUnitToResolved(*fledRanger, *unit);
            fledRanger->set_status(
                    net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT);
        }
    }
}

auto UpdateGameStatusAction::InternalExecute(
        const GameStateW& currentState,
        const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
    vector<ActionResult> results{};

    // If the game has already ended, just return that state
    if (GameIsOver(gameState->status())) { return results; }
    if (gameState->status()->state() ==
        net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
        return results;
    }

    // If there are unplaced heroes, we're still in setUp
    for (const auto& unit : *gameState->units()) {
        if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;

        if (unit->location().row() < 0) {
            // we should still be in setUp
            if (gameState->status()->state() !=
                net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
                throw ShardokInternalErrorException(
                        "There are unplaced units, but we're not in SET_UP");
            } else
                return results;
        }
    }

    auto survivors = SurvivingPlayers();
    if (survivors.size() == 1) {
        // If there's only one player, we can look for a LAST_PLAYER_STANDING victory
        const PlayerId survivor = *survivors.begin();
        const auto& winnerInfo = GetPlayerInfo(survivor);
        if (HasVictoryCondition(
                    winnerInfo,
                    net::eagle0::shardok::storage::fb::
                            VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING)) {
            results.emplace_back();
            results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
            results[0].mutable_game_status()->set_state(GameStatusProto::VICTORY);
            results[0].mutable_game_status()->add_winning_shardok_ids(survivor);
            results[0].mutable_game_status()->mutable_end_game_condition()->set_victory(
                    net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
            if (winnerInfo->is_defender()) {
                results[0].mutable_game_status()->set_description(
                        "Defender victory! The attacker is completely destroyed");
            } else {
                results[0].mutable_game_status()->set_description(
                        "Attacker victory! The defender is completely destroyed");
            }
            ResolveHiddenLosers(results[0]);

            return results;
        }
    } else {
        // Look for an alliance victory. That can only happen if each surviving player has the
        // victory condition set, and is allied with each other surviving player.

        bool allAllied = true;
        for (const PlayerId pid1 : survivors) {
            const auto& p1Info = GetPlayerInfo(pid1);
            if (!HasVictoryCondition(
                        p1Info,
                        net::eagle0::shardok::storage::fb::
                                VictoryCondition_VICTORY_CONDITION_LAST_ALLIANCE_STANDING)) {
                allAllied = false;
                break;
            }

            for (const PlayerId pid2 : survivors) {
                if (pid1 == pid2) continue;
                if (!common::ContainsWhere(
                            *p1Info->allies(),
                            [pid2](const net::eagle0::shardok::storage::fb::AlliedPlayer*
                                           alliedPlayer) {
                                return alliedPlayer->player_id() == pid2;
                            })) {
                    allAllied = false;
                    break;
                }
            }

            if (!allAllied) break;
        }

        if (allAllied) {
            results.emplace_back();
            results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
            results[0].mutable_game_status()->set_state(GameStatusProto::VICTORY);
            *results[0].mutable_game_status()->mutable_winning_shardok_ids() = {
                    std::begin(survivors),
                    std::end(survivors)};
            results[0].mutable_game_status()->mutable_end_game_condition()->set_victory(
                    net::eagle0::shardok::common::VICTORY_CONDITION_LAST_ALLIANCE_STANDING);
            results[0].mutable_game_status()->set_description(
                    "Alliance victory! The enemies of this alliance have been completely "
                    "destroyed");
            ResolveHiddenLosers(results[0]);

            return results;
        }
    }

    // The rest of the victory conditions only apply at the end of the round, so that a defender has
    // a chance to re-occupy castles, for instance
    if (!atEndOfRound) { return results; }

    // Check for time up
    if (gameState->current_round() > settingsGetter.Backing().max_rounds()) {
        for (const auto* pi : *gameState->player_infos()) {
            if (HasVictoryCondition(
                        pi,
                        net::eagle0::shardok::storage::fb::
                                VictoryCondition_VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS)) {
                results.emplace_back();
                results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
                results[0].mutable_game_status()->set_state(GameStatusProto::VICTORY);
                results[0].mutable_game_status()->add_winning_shardok_ids(pi->player_id());
                results[0].mutable_game_status()->mutable_end_game_condition()->set_victory(
                        net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
                results[0].mutable_game_status()->set_description(
                        "Defender victory! Survived past " +
                        std::to_string(settingsGetter.Backing().max_rounds()) + " rounds");
                ResolveHiddenLosers(results[0]);

                return results;
            }
        }

        results.emplace_back();
        results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
        results[0].mutable_game_status()->set_state(GameStatusProto::DRAW);
        results[0].mutable_game_status()->clear_winning_shardok_ids();
        results[0].mutable_game_status()->mutable_end_game_condition()->set_draw(
                net::eagle0::shardok::common::DRAW_AFTER_MAX_ROUNDS);
        results[0].mutable_game_status()->set_description(
                "Draw! No victory after " + std::to_string(settingsGetter.Backing().max_rounds()) +
                " rounds");

        return results;
    }

    // check for castle occupation
    if (!criticalTileLocations.empty()) {
        for (const auto* pi : *gameState->player_infos()) {
            if (HasVictoryCondition(
                        pi,
                        net::eagle0::shardok::storage::fb::
                                VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES)) {
                // Check for attacker occupying castles & towns
                bool foundUncontrolledCriticalTile = false;
                for (const auto& criticalTile : criticalTileLocations) {
                    const auto* possibleOccupant = currentState.GetOccupant(criticalTile);

                    if (!possibleOccupant) {
                        foundUncontrolledCriticalTile = true;
                        break;
                    }
                    const auto& occupant = possibleOccupant;

                    // Check if unit belongs to attacker
                    if (occupant->player_id() != pi->player_id()) {
                        foundUncontrolledCriticalTile = true;
                        break;
                    }

                    // Check if unit has a hero
                    if (!occupant->has_attached_hero()) {
                        foundUncontrolledCriticalTile = true;
                        break;
                    }
                }

                if (!foundUncontrolledCriticalTile) {
                    results.emplace_back();
                    results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
                    results[0].mutable_game_status()->set_state(
                            GameStatusProto::State::GameStatus_State_VICTORY);
                    results[0].mutable_game_status()->add_winning_shardok_ids(pi->player_id());
                    results[0].mutable_game_status()->mutable_end_game_condition()->set_victory(
                            net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
                    results[0].mutable_game_status()->set_description(
                            "Attacker victory! Attacker controls all critical tiles");
                    ResolveHiddenLosers(results[0]);
                    return results;
                }
            }
        }
    }

    // If we got all the way through, keep the status as it is
    return results;
}
}  // namespace shardok