//
// Created by Dan Crosby on 4/7/21.
//

#include "AIUnitScoreCalculator.hpp"

#include <algorithm>

#include "AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"

namespace shardok {

using std::begin;
using std::end;
using std::shared_ptr;

constexpr double kProfessionValue = 200;
constexpr double kCastleMultiplierBonus = 1.0;
constexpr double kOnFireMultiplier = 0.25;
constexpr double kAdjacentFireMultiplier = 0.99;
constexpr double kOnIceMultiplier = 0.25;
constexpr double kMeteorStartInRangeValue = 50;
constexpr double kMeteorDirectTargetingEnemy = 2;
constexpr double kMeteorDirectTargetingFriendly = -4;
constexpr double kMeteorSplashTargetingEnemy = 1;
constexpr double kMeteorSplashTargetingFriendly = -2;
constexpr double kMeteorNoneRoundMultiplier = 0.3;
constexpr double kMeteorStartRoundMultiplier = 0.8;
constexpr double kMeteorCastRoundMultiplier = 1.5;
constexpr double kMeteorCastInsufficientVigorMultiplier = 0.5;
constexpr double kMeteorCastleMultiplier = 2.0;
constexpr double kLightningPossibleValue = 0.05;
constexpr double kArcheryPossibleValue = 38;
constexpr double kReduceFortifiedPossibleValue = 50;
constexpr double kReduceUnfortifiedPossibleValue = 12;
constexpr double kFearPossibleValue = 38;
constexpr double kControlledThisRoundValue = 50;
constexpr double kDefenderThatAttackerMustKillValue = 200;
constexpr double kHeroExistenceBuf = 50;

constexpr int kVipInDangerMinimumSize = 200;
constexpr double kVipInDangerScore = -200;

auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue {
    const double armament = unit->battalion().armament();
    const double training = unit->battalion().training();
    const double morale = unit->battalion().morale();

    const double professionValue =
            (unit->has_attached_hero() &&
                             unit->attached_hero().profession_info().profession() !=
                                     net::eagle0::shardok::storage::fb::Profession_NO_PROFESSION
                     ? kProfessionValue
                     : 0.0);

    double statsValue = 0;
    if (unit->has_attached_hero()) {
        const auto &hero = unit->attached_hero();
        statsValue = static_cast<double>(
                             hero.strength() + hero.agility() + hero.wisdom() + hero.charisma()) /
                     4.0;
    }

    const double vigorValue = unit->has_attached_hero() ? unit->attached_hero().vigor() : 0.0;

    double battalionTypeMultiplier = 1.0;
    switch (unit->battalion().type()) {
        case net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY:
            battalionTypeMultiplier = 1.0;
            break;

        case net::eagle0::shardok::storage::fb::BattalionTypeId_HEAVY_INFANTRY:
        case net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_CAVALRY:
            battalionTypeMultiplier = 1.5;
            break;

        case net::eagle0::shardok::storage::fb::BattalionTypeId_HEAVY_CAVALRY:
            battalionTypeMultiplier = 2.0;
            break;

        case net::eagle0::shardok::storage::fb::BattalionTypeId_LONGBOWMEN:
            battalionTypeMultiplier = 1.25;
            break;

        case net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD:
            battalionTypeMultiplier = 0.25;
            break;
    }

    const double battalionValue = battalionTypeMultiplier * (0.5 + armament / 100.0) *
                                  (0.5 + training / 100.0) * (0.5 + morale / 100.0) *
                                  unit->battalion().size();

    const double heroValue =
            std::max(0.0, kHeroExistenceBuf + statsValue + professionValue + vigorValue);

    return battalionValue + heroValue;
}

auto archeryValue(const Unit *unit) -> double {
    // TODO: make this depend on the value of the targets
    return kArcheryPossibleValue;
}

auto reduceValue(const Unit *unit, const Terrain *unitTerrain) -> double {
    // TODO: make this depend on value of reducing castle too
    if (unit->fortified() || unitTerrain->modifier().castle().present())
        return kReduceFortifiedPossibleValue;
    else if (AllowsFortification(unitTerrain))
        return kReduceUnfortifiedPossibleValue;
    else
        return 0.0;
}

auto fearValue(const Unit *unit) -> double {
    // TODO: make this depend on the value of the targets
    return kFearPossibleValue;
}

auto lightningValue(const Unit *unit) -> double {
    // TODO: make this depend on the value of the targets
    if (unit->attached_hero().profession_info().meteor_cast_state() ==
        net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE) {
        return kLightningPossibleValue * unit->battalion().size();
    }

    return 0.0;
}

auto meteorDropRawValue(
        const Coords &targetCoords,
        const HexMap *map,
        const vector<const Unit *> &enemyOccupants,
        const vector<const Unit *> &friendlyOccupants) -> double {
    double rawMeteorDropValue = 0.0;
    if (targetCoords.row() >= 0) {
        const double castleMultiplier = GetTerrain(map, targetCoords)->modifier().castle().present()
                                                ? kMeteorCastleMultiplier
                                                : 1.0;

        const int index = targetCoords.row() * map->column_count() + targetCoords.column();
        if (const Unit *directOccupant = enemyOccupants[index]) {
            rawMeteorDropValue += directOccupant->battalion().size() * kMeteorDirectTargetingEnemy *
                                  castleMultiplier;
        }
        if (const Unit *directFriendly = friendlyOccupants[index]) {
            rawMeteorDropValue += directFriendly->battalion().size() *
                                  kMeteorDirectTargetingFriendly * castleMultiplier;
        }

        for (const auto &splashCoords : HexMapUtils::GetAdjacentCoords(map, targetCoords)) {
            const auto splashCastleMultiplier =
                    GetTerrain(map, splashCoords)->modifier().castle().present()
                            ? kMeteorCastleMultiplier
                            : 1.0;

            const auto splashIndex =
                    splashCoords.row() * map->column_count() + splashCoords.column();
            if (const Unit *splashOccupant = enemyOccupants[splashIndex]) {
                rawMeteorDropValue += splashOccupant->battalion().size() *
                                      kMeteorSplashTargetingEnemy * splashCastleMultiplier;
            }

            if (const Unit *splashFriendly = friendlyOccupants[splashIndex]) {
                rawMeteorDropValue += splashFriendly->battalion().size() *
                                      kMeteorSplashTargetingFriendly * splashCastleMultiplier;
            }
        }
    } else {
        rawMeteorDropValue = kMeteorStartInRangeValue;
    }

    return rawMeteorDropValue;
}

auto bestTargetValue(
        const Unit *unit,
        const HexMap *map,
        const vector<const Unit *> &enemyOccupants,
        const vector<const Unit *> &friendlyOccupants,
        const int meteorRange) -> double {
    double maxValue = 0.0;

    for (const Coords &target : TilesWithinDistance(map, unit->location(), meteorRange)) {
        if (const double thisTargetValue =
                    meteorDropRawValue(target, map, enemyOccupants, friendlyOccupants);
            thisTargetValue > maxValue) {
            maxValue = thisTargetValue;
        }
    }

    return maxValue;
}

auto meteorValue(
        const Unit *unit,
        const int roundsRemaining,
        const HexMap *map,
        const vector<const Unit *> &enemyUnits,
        const vector<const Unit *> &friendlyUnits,
        const int meteorRange,
        const double minVigorToCast) -> double {
    const auto enemyOccupants = Occupants(enemyUnits, map->row_count(), map->column_count());
    const auto friendlyOccupants = Occupants(friendlyUnits, map->row_count(), map->column_count());

    switch (unit->attached_hero().profession_info().meteor_cast_state()) {
        case net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE:
        case net::eagle0::shardok::storage::fb::MultiroundMagicState_START: {
            if ((roundsRemaining > 8 ||
                 (unit->attached_hero().vigor() > 50 && roundsRemaining > 4))) {
                const double maxValue =
                        bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);

                double multiplier = kMeteorStartRoundMultiplier;
                if (unit->attached_hero().profession_info().meteor_cast_state() ==
                    net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE) {
                    double vigorFactor = 1.0;
                    if (unit->attached_hero().vigor() < minVigorToCast) {
                        vigorFactor = kMeteorCastInsufficientVigorMultiplier *
                                      unit->attached_hero().vigor() / minVigorToCast;
                    }
                    multiplier = kMeteorNoneRoundMultiplier * vigorFactor;
                }
                return maxValue * multiplier;
            } else {
                return 0.0;
            }
        }

        case net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET:
            if (unit->attached_hero().profession_info().cast_target().row() > -1) {
                return meteorDropRawValue(
                        unit->attached_hero().profession_info().cast_target(),
                        map,
                        enemyOccupants,
                        friendlyOccupants);
            } else {
                return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
            }
        case net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST:
            return kMeteorCastRoundMultiplier *
                   meteorDropRawValue(
                           unit->attached_hero().profession_info().cast_target(),
                           map,
                           enemyOccupants,
                           friendlyOccupants);

        default: break;
    }

    return 0.0;
}

auto GetRangedAttackBonus(
        const Unit *unit,
        const bool isAttacker,
        const HexMap *map,
        const Terrain *terrain,
        const AttackLocations &attackLocations,
        const int roundsRemaining,
        const vector<const Unit *> &attackerUnits,
        const vector<const Unit *> &defenderUnits,
        const int meteorRange,
        const double minVigorToCast) -> double {
    vector<double> rangedAttackValues{};

    const auto &enemyUnits = isAttacker ? defenderUnits : attackerUnits;
    const auto &friendlyUnits = isAttacker ? attackerUnits : defenderUnits;

    const auto &unitLocation = unit->location();

    if (unit->has_attached_hero() &&
        unit->attached_hero().profession_info().profession() ==
                net::eagle0::shardok::storage::fb::Profession_MAGE &&
        (attackLocations.MageLocations().Contains(unitLocation) ||
         unit->attached_hero().profession_info().meteor_cast_state() !=
                 net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE)) {
        if (unit->attached_hero().profession_info().meteor_cast_state() ==
            net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE) {
            rangedAttackValues.push_back(lightningValue(unit));
        } else {
            const double mv = meteorValue(
                    unit,
                    roundsRemaining,
                    map,
                    enemyUnits,
                    friendlyUnits,
                    meteorRange,
                    minVigorToCast);
            rangedAttackValues.push_back(mv);
        }
    }

    if (attackLocations.EngineerLocations().Contains(unitLocation) && unit->has_attached_hero() &&
        unit->attached_hero().profession_info().profession() ==
                net::eagle0::shardok::storage::fb::Profession_ENGINEER) {
        rangedAttackValues.push_back(reduceValue(unit, terrain));
    }

    if (attackLocations.NecromancerLocations().Contains(unitLocation) &&
        unit->has_attached_hero() &&
        unit->attached_hero().profession_info().profession() ==
                net::eagle0::shardok::storage::fb::Profession_NECROMANCER &&
        unit->attached_hero().control_info().controlled_unit_id() != -1) {
        rangedAttackValues.push_back(fearValue(unit));
    }

    if (attackLocations.ArcheryLocations().Contains(unitLocation) &&
        unit->volleys_remaining() > 0) {
        rangedAttackValues.push_back(archeryValue(unit));
    }

    // Longbowmen in castle can archer adjacent
    if (attackLocations.AdjacentLocations().Contains(unitLocation) &&
        unit->volleys_remaining() > 0 && terrain->modifier().castle().present() &&
        unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_LONGBOWMEN) {
        rangedAttackValues.push_back(archeryValue(unit));
    }

    if (rangedAttackValues.empty()) return 0.0;
    return *std::max_element(begin(rangedAttackValues), end(rangedAttackValues));
}

auto UnitValue(
        const Unit *unit,
        const bool isAttacker,
        const vector<const Unit *> &attackerUnits,
        const bool attackerWantsCastles,
        const bool includeCastleBonus,
        const vector<const Unit *> &defenderUnits,
        const HexMap *map,
        const int roundsRemaining,
        const AttackLocations &locationsThisSideCanAttackFrom,
        const CoordsSet &locationsInDangerFromEnemy,
        const ActionPointDistances *distances,
        const SettingsGetter &settings) -> ScoreValue {
    const auto &location = unit->location();
    if (location.row() < 0) return 0;  // unplaced unit

    const bool unitIsUndead =
            unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD;

    const int coordsIndex = location.row() * map->column_count() + location.column();
    const auto &terrain = map->terrain()->Get(coordsIndex);
    double castleMultiplier = 1.0;
    // Only give a multiplier for being in a castle if the castle is useful, and the unit is not
    // undead
    if (terrain->modifier().castle().present() && includeCastleBonus &&
        (!isAttacker || attackerWantsCastles) && !unitIsUndead) {
        castleMultiplier +=
                kCastleMultiplierBonus * (terrain->modifier().castle().integrity() + 25) / 100.0;
    }
    double onFireMultiplier = 1.0;
    if (terrain->modifier().fire().present() && (isAttacker || attackerWantsCastles)) {
        onFireMultiplier *= kOnFireMultiplier;
    }
    {
        for (const auto adjacentCoords = HexMapUtils::GetAdjacentCoords(map, location);
             const auto &c : adjacentCoords) {
            if (const auto &adjTerrain = GetTerrain(map, c);
                adjTerrain->modifier().fire().present()) {
                onFireMultiplier *= kAdjacentFireMultiplier;
            }
        }
    }
    const double onIceMultiplier =
            terrain->modifier().ice().present() && !terrain->modifier().bridge().present()
                    ? terrain->modifier().ice().integrity() * kOnIceMultiplier / 100.0
                    : 1.0;

    const double rangedAttackValue = GetRangedAttackBonus(
            unit,
            isAttacker,
            map,
            terrain,
            locationsThisSideCanAttackFrom,
            roundsRemaining,
            attackerUnits,
            defenderUnits,
            settings.Backing().meteor_range(),
            settings.Backing().meteor_cast_vigor_cost());

    // scouting values
    // attack range
    // defensive position
    // needs to cross river
    // is hidden
    // action points
    // stun
    // cast state
    // volleys remaining

    double existenceBuf = 0;
    if (!isAttacker) {
        // Defender gets a bonus just for existing if the capture can't capture all the castles
        if (!attackerWantsCastles && !unitIsUndead) {
            existenceBuf = kDefenderThatAttackerMustKillValue;
        }
    }

    ScoreValue vipInDangerUtility = 0.0;
    if (unit->has_attached_hero() && unit->attached_hero().is_vip() &&
        unit->battalion().size() < kVipInDangerMinimumSize &&
        locationsInDangerFromEnemy.Contains(unit->location())) {
        vipInDangerUtility = kVipInDangerScore;
    }

    ScoreValue utility =
            castleMultiplier * onFireMultiplier * onIceMultiplier * ContextFreeUnitValue(unit) +
            rangedAttackValue + existenceBuf + vipInDangerUtility;

    if (unitIsUndead) {
        if (const auto commandingUnitId = unit->commanding_unit_id(); commandingUnitId != -1) {
            const Unit *commandingUnit = nullptr;
            for (const Unit *attackerUnit : attackerUnits) {
                if (attackerUnit->unit_id() == commandingUnitId) {
                    commandingUnit = attackerUnit;
                    break;
                }
            }

            if (commandingUnit == nullptr) {
                for (const Unit *defenderUnit : defenderUnits) {
                    if (defenderUnit->unit_id() == commandingUnitId) {
                        commandingUnit = defenderUnit;
                        break;
                    }
                }
            }

            if (commandingUnit &&
                commandingUnit->status() ==
                        net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
                commandingUnit->attached_hero().control_info().controlled_this_round()) {
                utility += kControlledThisRoundValue;
            }
        }

        auto minimumDistance = 9999;
        // Undead value lowered depending on distance to enemy

        for (const auto &attackLocation : locationsThisSideCanAttackFrom.AdjacentLocations()) {
            if (const DIST_T distance = distances->Distance(location, attackLocation);
                distance >= 0 && distance < minimumDistance)
                minimumDistance = distance;
        }

        // If the distance is too high, undead is worth nothing
        utility = utility / (1 + minimumDistance);
    }
    return utility;
}
}  // namespace shardok
