//
//  FallIntoWaterAction.cpp
//  eagle0_engine
//
//  Created by Dan Crosby on 1/21/19.
//  Copyright © 2019 none. All rights reserved.
//

#include "FallIntoWaterAction.hpp"

#include <utility>

#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"

namespace shardok {
using namespace net::eagle0::shardok::common;

auto EscapeChance(
        const int base,
        const Terrain &terrain,
        const WeatherFb &currentWeather,
        const double agility,
        const double intelligence) -> PercentileRollOdds {
    int16_t terrainFactor = 0;
    switch (terrain.type()) {
        case net::eagle0::shardok::storage::fb::Terrain_::Type_PLAINS: terrainFactor = 0; break;

        case net::eagle0::shardok::storage::fb::Terrain_::Type_HILL: terrainFactor = -20; break;

        case net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST: terrainFactor = -10; break;

        case net::eagle0::shardok::storage::fb::Terrain_::Type_SWAMP: terrainFactor = -20; break;

        case net::eagle0::shardok::storage::fb::Terrain_::Type_CITY: terrainFactor = +20; break;

        case net::eagle0::shardok::storage::fb::Terrain_::Type_STILL_WATER:
        case net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER:
            terrainFactor =
                    terrain.modifier().ice().present() || terrain.modifier().bridge().present()
                            ? -20
                            : -1000;
            break;

        case net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN:
        case net::eagle0::shardok::storage::fb::Terrain_::Type_UNKNOWN:
        case net::eagle0::shardok::storage::fb::Terrain_::Type_NONE: terrainFactor = -1000; break;
    }

    if (terrain.modifier().castle().present()) { terrainFactor -= 30; }

    constexpr int16_t weatherFactor = 0;
    const int16_t windFactor = -currentWeather.wind().speed_in_mph() * 2;
    const std::vector<double> stats = {intelligence, agility};

    std::vector<OtherFactor> otherFactors;
    if (terrain.modifier().fire().present()) {
        otherFactors.push_back(MakeOtherFactor(-50, "fire"));
    }

    return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, otherFactors);
}

auto ToVector(const Units &units) -> vector<const Unit *> {
    auto v = vector<const Unit *>{};
    for (const Unit *u : units) { v.push_back(u); }
    return v;
}

auto FallIntoWaterAction::AdjacentsWithTerrain(
        const Coords &location,
        const GameState *gameState,
        const PlayerId playerId) -> vector<AdjacentWithTerrain> {
    const CoordsSet adjacentCoords = HexMapUtils::GetAdjacentCoords(gameState->hex_map(), location);

    vector<AdjacentWithTerrain> vec{};

    for (const Coords &adjCoords : adjacentCoords) {
        const auto *possibleOccupant = Occupant(gameState->units(), adjCoords);
        if (possibleOccupant &&
            (possibleOccupant->player_id() == playerId || !possibleOccupant->hidden())) {
            continue;
        }
        vec.emplace_back(adjCoords, *GetTerrain(gameState->hex_map(), adjCoords));
    }

    return vec;
}

FallIntoWaterAction::FallIntoWaterAction(
        const UnitId fallerId,
        const SettingsGetter &settingsGetter,
        const double penetratingColdDamagePerTroop,
        const int baseEscapeOdds)
    : fallerId(fallerId),
      settings(settingsGetter),
      penetratingColdDamagePerTroop(penetratingColdDamagePerTroop),
      baseEscapeOdds(baseEscapeOdds) {}

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

    auto fallerTerrain =
            *GetTerrain(currentState->hex_map(), currentState->units()->Get(fallerId)->location());
    auto adjacentCoordsAndTerrain = AdjacentsWithTerrain(
            currentState->units()->Get(fallerId)->location(),
            currentState,
            currentState->units()->Get(fallerId)->player_id());

    const Unit *fallerBefore = currentState->units()->Get(fallerId);
    const Coords &location = fallerBefore->location();

    int troops = fallerBefore->battalion().size();
    // Calculate damage from the fall
    CombatDamage damage = (CombatDamage::Builder()
                                   .SetPenetratingCold(penetratingColdDamagePerTroop * troops)
                                   .Build());

    auto fallerAfter = *fallerBefore;
    MutatingApplyEnvironmentalDamage(
            &fallerAfter,
            damage,
            fallerTerrain,
            settings.Backing().morale_shift_per_casualty(),
            settings.Backing().base_deadliness(),
            settings.GetBattalionType(fallerAfter.battalion().type()));

    fallerAfter.mutate_stun_rounds_remaining(fallerBefore->stun_rounds_remaining() + 1);

    ActionResult waterDamageResult;
    waterDamageResult.set_type(net::eagle0::shardok::common::WATER_DAMAGE);
    waterDamageResult.mutable_actor()->set_value(fallerId);
    waterDamageResult.mutable_player()->set_value(fallerAfter.player_id());
    *waterDamageResult.mutable_target_coords() = ToCoordsProto(location);
    AddChangedUnit(waterDamageResult, fallerAfter);

    results.push_back(waterDamageResult);

    if (IsDestroyed(fallerAfter)) { return results; }

    if (fallerAfter.has_attached_hero()) {
        const auto &fallerHero = fallerAfter.attached_hero();
        Coords bestCoords;
        bool found = false;
        int16_t bestOdds = 0;
        Terrain bestTerrain{};

        for (const auto &adjWithTerrain : adjacentCoordsAndTerrain) {
            const auto *possibleOccupant = currentState.GetOccupant(adjWithTerrain.adjacentCoords);
            if (possibleOccupant && (possibleOccupant->player_id() == fallerAfter.player_id() ||
                                     !possibleOccupant->hidden())) {
                continue;
            }

            const auto &adjacentTerrain = adjWithTerrain.terrain;
            PercentileRollOdds odds = EscapeChance(
                    baseEscapeOdds,
                    adjacentTerrain,
                    *currentState->weather(),
                    fallerHero.agility(),
                    fallerHero.wisdom());
            if (GetTotalOdds(odds) >= bestOdds) {
                bestOdds = GetTotalOdds(odds);
                bestCoords = adjWithTerrain.adjacentCoords;
                bestTerrain = adjWithTerrain.terrain;
                found = true;
            }
        }

        if (found && !currentState.GetOccupant(bestCoords)) {
            PercentileRollOdds odds = EscapeChance(
                    baseEscapeOdds,
                    bestTerrain,
                    *currentState->weather(),
                    fallerHero.agility(),
                    fallerHero.wisdom());

            auto escapeRoll = generator->Percentile();

            if (PercentileRollSucceeds(odds, escapeRoll)) {
                fallerAfter.mutable_location() = bestCoords;

                ActionResult escapeResult{};
                escapeResult.set_type(net::eagle0::shardok::common::ESCAPED_TO_SHORE);
                escapeResult.mutable_actor()->set_value(fallerId);
                *escapeResult.mutable_target_coords() = ToCoordsProto(bestCoords);
                AddChangedUnit(escapeResult, fallerAfter);

                results.push_back(escapeResult);

                return results;
            }
        }
    }

    ActionResult sweptAwayResult{};
    sweptAwayResult.set_type(net::eagle0::shardok::common::SWEPT_AWAY);
    sweptAwayResult.mutable_actor()->set_value(fallerId);
    auto *resolvedUnit = sweptAwayResult.add_resolved_units();
    resolvedUnit->set_status(net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT);
    AddUnitToResolved(*resolvedUnit, fallerAfter);

    results.push_back(sweptAwayResult);
    return results;
}
}  // namespace shardok
