//
// Created by Dan Crosby on 12/5/21.
//

#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateGuesser.hpp"

#include <vector>

#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"

namespace shardok {

using std::begin;
using std::end;
using std::vector;

using flatbuffers::FlatBufferBuilder;
using flatbuffers::Offset;

using BattalionViewProto = net::eagle0::shardok::api::BattalionView;
using HeroViewProto = net::eagle0::shardok::api::HeroView;
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
using UnitViewProto = net::eagle0::shardok::api::UnitView;

using Coords = net::eagle0::shardok::storage::fb::Coords;
using Unit = net::eagle0::shardok::storage::fb::Unit;

constexpr int8_t kGuessedStat = 50;

auto GuessedUnit(
        const SettingsGetter &settings,
        const UnitViewProto &uv,
        PlayerId toPid,
        Coords location,
        net::eagle0::shardok::storage::fb::UnitStatus status) -> Unit;

auto GuessedHero(const HeroViewProto &hv) -> net::eagle0::shardok::storage::fb::Hero;

auto GuessedBattalion(const BattalionViewProto &bv) -> net::eagle0::shardok::storage::fb::Battalion;

auto GameStateGuesser::GuessedState(
        PlayerId pid,
        const SettingsGetter &settings,
        const GameStateView &gameStateView) -> GameStateW {
    bool isDefender = false;
    bool defenderExists = false;
    for (const auto &pip : gameStateView.player_infos()) {
        defenderExists = defenderExists || pip.is_defender();
        isDefender = isDefender || (pip.is_defender() && pip.player_id() == pid);
    }

    FlatBufferBuilder fbb;
    fbb.ForceDefaults(true);

    auto gameIdOffset = fbb.CreateString(gameStateView.game_id());

    vector<Offset<net::eagle0::shardok::storage::fb::PlayerInfo>> piVec{};
    for (const PlayerInfoProto &pi : gameStateView.player_infos()) {
        piVec.push_back(fb::FromPlayerInfoProto(fbb, pi));
    }
    auto piOffset = fbb.CreateVectorOfSortedTables(&piVec);

    auto statusDescription = fbb.CreateString(gameStateView.status().description());

    vector<PlayerId> winningShardokIdsVec = {
            begin(gameStateView.status().winning_shardok_ids()),
            end(gameStateView.status().winning_shardok_ids())};
    for (int i = gameStateView.status().winning_shardok_ids_size(); i < 10; i++) {
        winningShardokIdsVec.push_back(-1);
    }
    auto winningShardokIdsOffset = fbb.CreateVector(winningShardokIdsVec);

    net::eagle0::shardok::storage::fb::GameStatusBuilder statusBuilder(fbb);
    statusBuilder.add_description(statusDescription);
    statusBuilder.add_state(static_cast<net::eagle0::shardok::storage::fb::GameStatus_::State>(
            gameStateView.status().state()));

    net::eagle0::shardok::storage::fb::EndGameCondition endGameCondition;
    if (gameStateView.status().has_end_game_condition()) {
        switch (gameStateView.status().end_game_condition().condition_case()) {
            case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kVictory:
                endGameCondition.mutate_victory_type(
                        net::eagle0::shardok::storage::fb::VictoryType_VICTORY);
                endGameCondition.mutate_victory_details(
                        static_cast<net::eagle0::shardok::storage::fb::VictoryCondition>(
                                gameStateView.status().end_game_condition().victory()));
                break;

            case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kAllyVictory:
                endGameCondition.mutate_victory_type(
                        net::eagle0::shardok::storage::fb::VictoryType_ALLY_VICTORY);
                endGameCondition.mutate_victory_details(
                        static_cast<net::eagle0::shardok::storage::fb::VictoryCondition>(
                                gameStateView.status().end_game_condition().ally_victory()));
                break;

            case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kDraw:
                endGameCondition.mutate_victory_type(
                        net::eagle0::shardok::storage::fb::VictoryType_DRAW);
                endGameCondition.mutate_draw_details(
                        static_cast<net::eagle0::shardok::storage::fb::DrawType>(
                                gameStateView.status().end_game_condition().draw()));
                break;

            case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kLoss:
                endGameCondition.mutate_victory_type(
                        net::eagle0::shardok::storage::fb::VictoryType_LOSS);
                endGameCondition.mutate_victory_details(
                        static_cast<net::eagle0::shardok::storage::fb::VictoryCondition>(
                                gameStateView.status().end_game_condition().loss()));
                break;

            case net::eagle0::shardok::common::EndGameCondition::ConditionCase::CONDITION_NOT_SET:
                break;
        }
    }

    statusBuilder.add_end_game_condition(&endGameCondition);
    statusBuilder.add_winning_shardok_ids(winningShardokIdsOffset);
    auto statusOffset = statusBuilder.Finish();

    auto mapOffset = fb::ConvertHexMapProto(fbb, gameStateView.hex_map());

    // units
    bool foundDefender = false;
    UnitId maxId = 0;

    vector<UnitId> foundUnits{};
    vector<Unit> unitsVec{};
    for (const auto &uv : gameStateView.units()) {
        unitsVec.push_back(GuessedUnit(
                settings,
                uv,
                pid,
                FromCoordsProto(uv.location()),
                net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT));
        if (uv.player_id() == 1) foundDefender = true;
        if (uv.unit_id() > maxId) maxId = uv.unit_id();
        foundUnits.push_back(uv.unit_id());
    }

    for (const auto &uv : gameStateView.reserve_units()) {
        Coords location = FromCoordsProto(uv.location());
        auto status = net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;

        // If the player is not the defender and there are no known defenders, guess one
        if (!isDefender && defenderExists) {
            if (!foundDefender && uv.player_id() != pid) {
                foundDefender = true;
                status = net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT;
                for (const auto &startPosProto :
                     gameStateView.hex_map().defender_starting_positions().positions()) {
                    Coords startPos = FromCoordsProto(startPosProto);
                    if (!Occupant<UnitViewProto>(gameStateView.units(), startPos).has_value()) {
                        location = startPos;
                        break;
                    }
                }
            }
        }
        unitsVec.push_back(GuessedUnit(settings, uv, pid, location, status));

        if (uv.unit_id() > maxId) maxId = uv.unit_id();
        foundUnits.push_back(uv.unit_id());
    }

    for (int i = 0; i < maxId; i++) {
        auto entry = std::find(std::begin(foundUnits), std::end(foundUnits), i);
        if (entry == std::end(foundUnits)) {
            net::eagle0::shardok::storage::fb::Unit newUnit;
            newUnit.mutate_unit_id(i);
            newUnit.mutate_has_attached_hero(false);
            newUnit.mutate_commanding_unit_id(-1);
            newUnit.mutate_player_id(0);
            newUnit.mutable_battalion().mutate_type(
                    net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY);
            newUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT);
            newUnit.mutable_location() = Coords(-1, -1);

            unitsVec.push_back(newUnit);
        }
    }

    // If we're the attacker and we don't know of defenders, we're either in SETUP or defenders are
    // all hidden
    if (!isDefender && !foundDefender && defenderExists) {
        Unit newUnit;

        newUnit.mutable_battalion().mutate_size(1000);

        newUnit.mutate_unit_id(maxId + 1);
        newUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);

        for (const auto &startPosProto :
             gameStateView.hex_map().defender_starting_positions().positions()) {
            Coords startPos = FromCoordsProto(startPosProto);
            if (!Occupant<UnitViewProto>(gameStateView.units(), startPos).has_value()) {
                newUnit.mutable_location() = startPos;
                newUnit.mutate_hidden(true);
                newUnit.mutate_player_id(1);
                break;
            }
        }

        unitsVec.push_back(newUnit);
    }

    // Add 5 reserved slots for future expansion (e.g., necromancer raise dead)
    UnitId nextReservedId = maxId + 2;  // Start after any guessed defender unit
    for (int i = 0; i < 5; i++) {
        Unit reservedSlot;
        reservedSlot.mutate_unit_id(nextReservedId + i);
        reservedSlot.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT);
        // Set other required fields to safe defaults
        reservedSlot.mutate_player_id(-1);
        reservedSlot.mutate_eagle_player_id(-1);
        reservedSlot.mutable_location().mutate_row(-1);
        reservedSlot.mutable_location().mutate_column(-1);
        reservedSlot.mutate_has_attached_hero(false);
        reservedSlot.mutate_commanding_unit_id(-1);
        unitsVec.push_back(reservedSlot);
    }

    auto unitsOffset = fbb.CreateVectorOfSortedStructs(&unitsVec);

    auto chargeeIdsVec = std::vector<UnitId>(
            begin(gameStateView.possible_chargee_ids()),
            end(gameStateView.possible_chargee_ids()));
    for (int i = gameStateView.possible_chargee_ids_size(); i < 6; i++) {
        chargeeIdsVec.push_back(-1);
    }
    auto chargeeIdsOffset = fbb.CreateVector(chargeeIdsVec);

    net::eagle0::shardok::storage::fb::GameStateBuilder gs(fbb);

    // constants
    gs.add_game_id(gameIdOffset);
    gs.add_month(gameStateView.month());

    // these change, but they're well-known
    gs.add_current_round(gameStateView.current_round());
    gs.add_current_player(gameStateView.current_player());

    gs.add_player_infos(piOffset);

    net::eagle0::shardok::storage::fb::Wind wind(
            static_cast<net::eagle0::shardok::storage::fb::HexMapDirection>(
                    gameStateView.weather().wind().direction()),
            gameStateView.weather().wind().speed_in_mph());

    net::eagle0::shardok::storage::fb::Weather weather(
            static_cast<net::eagle0::shardok::storage::fb::WeatherConditions>(
                    gameStateView.weather().conditions()),
            wind);
    gs.add_weather(&weather);

    gs.add_status(statusOffset);

    // partially known
    gs.add_hex_map(mapOffset);

    gs.add_units(unitsOffset);

    if (gameStateView.has_eligible_charger_id()) {
        gs.add_eligible_charger_id(gameStateView.eligible_charger_id().value());
    } else {
        gs.add_eligible_charger_id(-1);
    }

    gs.add_possible_chargee_ids(chargeeIdsOffset);

    fbb.Finish(gs.Finish());

    GameStateW gsw(fbb);
    // Make sure that controlled unit/commanding unit IDs match up
    for (UnitId unitId = 0; unitId < gsw->units()->size(); unitId++) {
        const Unit *unit = gsw->units()->Get(unitId);

        if (unit->has_attached_hero()) {
            UnitId controlledUnitId = unit->attached_hero().control_info().controlled_unit_id();
            if (controlledUnitId != -1) {
                gsw->mutable_units()
                        ->GetMutableObject(controlledUnitId)
                        ->mutate_commanding_unit_id(unitId);
            }
        }

        UnitId commandingUnitId = unit->commanding_unit_id();
        if (unit->commanding_unit_id() != -1) {
            gsw->mutable_units()
                    ->GetMutableObject(commandingUnitId)
                    ->mutable_attached_hero()
                    .mutable_control_info()
                    .mutate_controlled_unit_id(unitId);
        }
    }

    return gsw;
}

auto GuessedUnit(
        const SettingsGetter &settings,
        const UnitViewProto &uv,
        PlayerId toPid,
        Coords location,
        net::eagle0::shardok::storage::fb::UnitStatus status) -> Unit {
    net::eagle0::shardok::storage::fb::Hero attachedHero{};
    if (uv.has_attached_hero()) { attachedHero = GuessedHero(uv.attached_hero()); }

    net::eagle0::shardok::storage::fb::Unit unit;

    unit.mutate_unit_id(uv.unit_id());
    unit.mutate_player_id(uv.player_id());
    unit.mutate_hidden(uv.hidden());
    if (uv.has_attached_hero()) {
        unit.mutable_attached_hero() = attachedHero;
        unit.mutate_has_attached_hero(true);
    } else {
        unit.mutate_has_attached_hero(false);
    }
    if (uv.has_commanding_unit_id()) {
        unit.mutate_commanding_unit_id(uv.commanding_unit_id().value());
    } else {
        unit.mutate_commanding_unit_id(-1);
    }

    unit.mutable_location() = location;

    // We might have an off-map location for a normal unit if it's hidden. We'll just pretend
    // it's a reserve unit for now. Ideally we'd make a guess as to where it's hidden.
    if (location.row() < 0 && status == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
        // I *think* hidden units are the only way this can happen, let's check.
        assert(uv.hidden());
        unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
    } else {
        unit.mutate_status(status);
    }

    unit.mutable_battalion() = GuessedBattalion(uv.battalion());

    if (uv.has_remaining_action_points()) {
        unit.mutate_remaining_action_points(uv.remaining_action_points().value());
    } else {
        unit.mutate_remaining_action_points(0);
    }
    if (uv.has_stun_rounds_remaining()) {
        unit.mutate_stun_rounds_remaining(uv.stun_rounds_remaining().value());
    } else {
        unit.mutate_stun_rounds_remaining(0);
    }
    if (uv.has_volleys_remaining()) {
        unit.mutate_volleys_remaining(uv.volleys_remaining().value());
    } else {
        unit.mutate_volleys_remaining(0);
    }

    unit.mutate_has_moved_in_zoc(uv.has_moved_in_zoc());

    for (int i = 0; i < unit.opponent_knowledge()->size(); i++) {
        unit.mutable_opponent_knowledge()->Mutate(i, 0);
    }
    unit.mutable_opponent_knowledge()->Mutate(toPid, uv.my_knowledge());

    unit.mutate_can_flee(uv.can_flee());
    unit.mutate_can_archery(uv.can_archery());
    unit.mutate_can_start_fire(uv.can_start_fire());
    unit.mutate_fortified(uv.fortified());
    unit.mutate_targeted_unit(-1);

    if (uv.has_starting_position_index()) {
        unit.mutate_starting_position_index(uv.starting_position_index().value());
    } else {
        unit.mutate_starting_position_index(-1);
    }

    return unit;
}

auto GuessedHero(const HeroViewProto &hv) -> net::eagle0::shardok::storage::fb::Hero {
    net::eagle0::shardok::storage::fb::Hero hero;

    hero.mutate_eagle_hero_id(-1);

    if (hv.has_control_info()) {
        hero.mutable_control_info().mutate_controlled_unit_id(
                hv.control_info().controlled_unit_id());
        hero.mutable_control_info().mutate_controlled_this_round(
                hv.control_info().controlled_this_round());
    } else {
        hero.mutable_control_info().mutate_controlled_unit_id(-1);
        hero.mutable_control_info().mutate_controlled_this_round(false);
    }

    hero.mutate_is_vip(hv.has_is_vip() && hv.is_vip().value());

    if (hv.has_stats()) {
        const auto &stats = hv.stats();
        hero.mutate_strength(stats.strength());
        hero.mutate_strength_xp(kGuessedStat);
        hero.mutate_agility(stats.agility());
        hero.mutate_agility_xp(kGuessedStat);
        hero.mutate_constitution(stats.constitution());
        hero.mutate_constitution_xp(kGuessedStat);
        hero.mutate_charisma(stats.charisma());
        hero.mutate_charisma_xp(kGuessedStat);
        hero.mutate_wisdom(stats.wisdom());
        hero.mutate_wisdom_xp(kGuessedStat);

        hero.mutate_integrity(stats.integrity());
        hero.mutate_ambition(stats.ambition());
        hero.mutate_gregariousness(stats.gregariousness());
        hero.mutate_bravery(stats.bravery());

        hero.mutate_vigor(stats.vigor());
        hero.mutate_starting_vigor(stats.starting_vigor());
        hero.mutate_spent_vigor(kGuessedStat);
    } else {
        // Don't believe it's destroyed
        hero.mutate_strength(kGuessedStat);
        hero.mutate_strength_xp(kGuessedStat);
        hero.mutate_agility(kGuessedStat);
        hero.mutate_agility_xp(kGuessedStat);
        hero.mutate_constitution(kGuessedStat);
        hero.mutate_charisma(kGuessedStat);
        hero.mutate_charisma_xp(kGuessedStat);
        hero.mutate_wisdom(kGuessedStat);
        hero.mutate_wisdom_xp(kGuessedStat);

        hero.mutate_integrity(kGuessedStat);
        hero.mutate_ambition(kGuessedStat);
        hero.mutate_gregariousness(kGuessedStat);
        hero.mutate_bravery(kGuessedStat);

        hero.mutate_vigor(kGuessedStat);
        hero.mutate_starting_vigor(kGuessedStat);
        hero.mutate_spent_vigor(kGuessedStat);
    }

    if (hv.has_profession_info()) {
        hero.mutable_profession_info().mutate_profession(
                static_cast<net::eagle0::shardok::storage::fb::Profession>(
                        hv.profession_info().profession()));
        hero.mutable_profession_info().mutate_meteor_cast_state(
                static_cast<net::eagle0::shardok::storage::fb::MultiroundMagicState>(
                        hv.profession_info().meteor_cast_state()));
        if (hv.profession_info().has_cast_target()) {
            hero.mutable_profession_info().mutable_cast_target().mutate_row(
                    hv.profession_info().cast_target().row());
            hero.mutable_profession_info().mutable_cast_target().mutate_column(
                    hv.profession_info().cast_target().column());
        } else {
            hero.mutable_profession_info().mutable_cast_target().mutate_row(-1);
            hero.mutable_profession_info().mutable_cast_target().mutate_column(-1);
        }
    } else {
        hero.mutable_profession_info().mutable_cast_target().mutate_row(-1);
        hero.mutable_profession_info().mutable_cast_target().mutate_column(-1);
        hero.mutable_profession_info().mutate_profession(
                net::eagle0::shardok::storage::fb::Profession_NO_PROFESSION);
        hero.mutable_profession_info().mutate_meteor_cast_state(
                net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
    }

    return hero;
}

auto GuessedBattalion(const BattalionViewProto &bv)
        -> net::eagle0::shardok::storage::fb::Battalion {
    const net::eagle0::shardok::storage::fb::Battalion batt(
            static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(bv.type()),
            bv.size(),
            bv.has_morale() ? bv.morale().value() : kGuessedStat,
            bv.has_training() ? bv.training().value() : kGuessedStat,
            bv.has_armament() ? bv.armament().value() : kGuessedStat,
            -1,
            kGuessedStat);

    return batt;
}

}  // namespace shardok