//
//  Unit.cpp
//  eagle0
//
//  Created by Dan Crosby on 8/20/18.
//  Copyright © 2018 none. All rights reserved.
//

#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"

#include <utility>

namespace shardok {
#define ACTION_COST_TO_VIGOR_COST ((0.50))

auto InternalTakeDamageFb(
        const Unit *victim,
        const CombatDamage &totalDamage,
        const BattalionTypeSPtr &battalionType,
        const TerrainProto &terrain,
        double heroDamageRatio,
        double flankingAdjustment,
        double heroMortalityFactor,
        double baseDeadliness) -> Unit;

inline auto factorIntoAdjustment(const double factorPercentile, const double swingPercentile)
        -> double {
    return 1.0 + (factorPercentile * swingPercentile / 100.0) / 100.0;
}

auto MaxActionPoints(const Unit &unit) -> ActionPoints {
    ActionPoints actionPoints = 5;
    if (unit.has_attached_hero()) {
        actionPoints += ActionPoints(lround(unit.attached_hero().vigor() / 25.0));
    }
    actionPoints += ActionPoints(lround(unit.battalion().morale() / 25.0));

    return actionPoints;
}

auto OpponentKnowledge(const Unit &unit, const PlayerId toPlayer) -> int {
    if (unit.opponent_knowledge()->size() == 0) return 0;
    else if (toPlayer == -1) {
        // handle observer, with pid -1
        int8_t max = 0;
        for (int i = 0; i < unit.opponent_knowledge()->size(); i++) {
            max = std::max(max, unit.opponent_knowledge()->Get(i));
        }
        return max;
    } else
        return unit.opponent_knowledge()->Get(toPlayer);
}

auto OpponentKnowledge(const Unit *unit, const PlayerId toPlayer) -> int {
    if (unit->opponent_knowledge()->size() == 0) return 0;
    else if (toPlayer >= unit->opponent_knowledge()->size())
        return 0;
    else if (toPlayer == -1) {
        // handle observer, with pid -1
        int8_t max = 0;
        for (int i = 0; i < unit->opponent_knowledge()->size(); i++) {
            max = std::max(max, unit->opponent_knowledge()->Get(i));
        }
        return max;
    } else
        return unit->opponent_knowledge()->Get(toPlayer);
}

auto MutatingApplyDamageFrom(
        Unit *recipient,
        Unit *from,
        const CombatDamage &damage,
        const Terrain &terrain,
        const AttackOrientation direction,
        const bool isAmbush,
        const double moralePerCasualty,
        const double baseDeadliness,
        const BattalionTypeSPtr &recipientType,
        const BattalionTypeSPtr &fromType) -> int32_t {
    const CombatDamage battalionDamage = recipient->has_attached_hero()
                                                 ? MutatingTakeDamage(
                                                           &recipient->mutable_attached_hero(),
                                                           damage,
                                                           recipient->battalion().size(),
                                                           isAmbush)
                                                 : damage;

    return MutatingTakeDamageFrom(
            &recipient->mutable_battalion(),
            &from->mutable_battalion(),
            battalionDamage,
            terrain,
            direction,
            recipient->has_attached_hero() ? recipient->attached_hero().bravery() : 50.0,
            moralePerCasualty,
            baseDeadliness,
            recipientType,
            fromType);
}

auto MutatingApplyEnvironmentalDamage(
        Unit *recipient,
        const CombatDamage &damage,
        const Terrain &terrain,
        const double moralePerCasualty,
        const double baseDeadliness,
        const BattalionTypeSPtr &type) -> int32_t {
    const CombatDamage battalionDamage = recipient->has_attached_hero()
                                                 ? MutatingTakeDamage(
                                                           &recipient->mutable_attached_hero(),
                                                           damage,
                                                           recipient->battalion().size(),
                                                           false)
                                                 : damage;

    return MutatingTakeEnvironmentalDamage(
            &recipient->mutable_battalion(),
            battalionDamage,
            terrain,
            moralePerCasualty,
            baseDeadliness,
            type);
}

void MutatingSpendActionPoints(Unit *unit, const ActionCost &cost) {
    if (cost.points > unit->remaining_action_points()) {
        throw ShardokInternalErrorException("Trying to spend more action points than the unit has");
    }
    if (cost.type == ActionCost::impossible) {
        throw ShardokInternalErrorException("Trying to spend an impossible action cost");
    }
    if (cost.type == ActionCost::usesAll) {
        unit->mutate_remaining_action_points(0);
    } else {
        if (unit->remaining_action_points() < cost.points) {
            throw ShardokInternalErrorException("Trying to spend more action points than we have");
        }
        unit->mutate_remaining_action_points(unit->remaining_action_points() - cost.points);
    }

    if (unit->has_attached_hero()) {
        const double currentVigor = unit->attached_hero().vigor();
        const double vigorSpend = -(double)cost.points * ACTION_COST_TO_VIGOR_COST;

        const double newVigor = std::max(currentVigor + vigorSpend, 0.0);
        unit->mutable_attached_hero().mutate_vigor(newVigor);
    }
}

void MutatingAdjustMorale(Unit *unit, const BattalionTypeSPtr &battalionType, const double amount) {
    if (battalionType->adjustsMorale) {
        const auto newMorale = std::min(100.0, std::max(0.0, unit->battalion().morale() + amount));
        unit->mutable_battalion().mutate_morale(newMorale);
    }
}

void MutatingBumpStrengthXp(Unit *unit, const int newXp) {
    if (unit->has_attached_hero()) {
        unit->mutable_attached_hero().mutate_strength_xp(
                unit->attached_hero().strength_xp() + newXp);
    }
}

void MutatingBumpAgilityXp(Unit *unit, const int newXp) {
    if (unit->has_attached_hero()) {
        unit->mutable_attached_hero().mutate_agility_xp(unit->attached_hero().agility_xp() + newXp);
    }
}

void MutatingBumpWisdomXp(Unit *unit, const int newXp) {
    if (unit->has_attached_hero()) {
        unit->mutable_attached_hero().mutate_wisdom_xp(unit->attached_hero().wisdom_xp() + newXp);
    }
}

void MutatingBumpCharismaXp(Unit *unit, const int newXp) {
    if (unit->has_attached_hero()) {
        unit->mutable_attached_hero().mutate_charisma_xp(
                unit->attached_hero().charisma_xp() + newXp);
    }
}

void MutatingBumpOpponentKnowledge(Unit *unit, const PlayerId pid, const int bump) {
    if (pid < 0 || pid >= unit->opponent_knowledge()->size()) return;
    const int32_t currentKnowledge = OpponentKnowledge(unit, pid);
    const int32_t newKnowledge = std::min(100, currentKnowledge + bump);
    unit->mutable_opponent_knowledge()->Mutate(pid, newKnowledge);
}

void MutatingBumpAllKnowledgeToMinimum(Unit *unit, const int minimum) {
    const auto current = unit->opponent_knowledge();
    for (int i = 0; i < current->size(); i++) {
        if (current->Get(i) < minimum) { unit->mutable_opponent_knowledge()->Mutate(i, minimum); }
    }
}

auto IsDestroyed(const Unit &unit) -> bool {
    return unit.has_attached_hero() ? shardok::IsDestroyed(unit.attached_hero())
                                    : unit.battalion().size() == 0;
}

auto InternalTakeDamageFb(
        const Unit *victim,
        const CombatDamage &totalDamage,
        const BattalionTypeSPtr &battalionType,
        const Terrain &terrain,
        double heroDamageRatio,
        const double flankingAdjustment,
        const double heroMortalityFactor,
        const double baseDeadliness) -> Unit {
    Unit result = *victim;

    const auto battalion = victim->battalion();

    if (heroDamageRatio > 1.0) heroDamageRatio = 1.0;
    if (heroDamageRatio < 0.0) heroDamageRatio = 0.0;

    if (victim->has_attached_hero()) {
        const auto hero = victim->attached_hero();
        CombatDamage heroDamage = heroDamageRatio * totalDamage;

        double vigorHit = 0.0;
        for (int i = 0; i < NUM_DAMAGE_TYPES; i++) {
            vigorHit += heroDamage.GetNormalDamageOfType(DamageType(i)) * heroMortalityFactor;
            vigorHit += heroDamage.GetPenetratingDamageOfType(DamageType(i)) * heroMortalityFactor;
            // TODO: hero should have adjustments for damage types too
        }

        if (vigorHit > hero.vigor()) vigorHit = hero.vigor();

        result.mutable_attached_hero().mutate_vigor(hero.vigor() - vigorHit);
    }

    CombatDamage battalionDamage = (1.0 - heroDamageRatio) * totalDamage;

    double takenDamage = 0.0;
    double terrainAdjustment = battalionType->GetDamageTakenMultiplierForTerrain(&terrain);

    for (int i = 0; i < (int)NUM_DAMAGE_TYPES; i++) {
        double armamentAdjustment = 1.0 - (battalionType->baseResistanceByType[i] +
                                           (battalion.armament() / 100.0) *
                                                   battalionType->maxAugmentResistanceByType[i]) /
                                                  100.0;
        double newDamage =
                (battalionDamage.GetNormalDamageOfType((DamageType)i) * flankingAdjustment *
                 armamentAdjustment);

        newDamage += battalionDamage.GetPenetratingDamageOfType((DamageType)i) * flankingAdjustment;

        if (DamageTypeGetsTerrainDefense((DamageType)i)) { newDamage *= terrainAdjustment; }

        takenDamage += newDamage;
    }

    int32_t newNumber;
    auto casualties = (int32_t)(takenDamage * baseDeadliness);

    if (battalion.size() > casualties) {
        newNumber = battalion.size() - casualties;
    } else {
        newNumber = 0;
    }

    result.mutable_battalion().mutate_size(newNumber);

    return result;
}

auto WithTakenDamageFb(
        const Unit *victim,
        const CombatDamage &amount,
        const Terrain &terrain,
        const AttackOrientation orientation,
        const bool isAmbush,
        const bool shouldReduceForFortified,
        const SettingsGetter &settings) -> Unit {
    const double heroDamageRatio =
            (victim->has_attached_hero()
                     ? (factorIntoAdjustment(
                                victim->attached_hero().bravery(),
                                settings.Backing().hero_bravery_swing()) *
                        (isAmbush ? settings.Backing().hero_damage_ambush_adjustment() : 1.0) /
                        (victim->battalion().size() + 1))
                     : 0.0);

    double flankingAdjustment = 1.0;
    const BattalionTypeSPtr &type = settings.GetBattalionType(victim->battalion().type());
    if (orientation == flank) flankingAdjustment = type->flankDamageMultiplier;
    else if (orientation == rear)
        flankingAdjustment = type->rearDamageMultiplier;

    if (shouldReduceForFortified && victim->fortified()) {
        return InternalTakeDamageFb(
                victim,
                amount * settings.Backing().fortify_damage_taken_multiplier(),
                settings.GetBattalionType(victim->battalion().type()),
                terrain,
                heroDamageRatio,
                flankingAdjustment,
                settings.Backing().hero_mortality_factor(),
                settings.Backing().base_deadliness());
    } else {
        return InternalTakeDamageFb(
                victim,
                amount,
                settings.GetBattalionType(victim->battalion().type()),
                terrain,
                heroDamageRatio,
                flankingAdjustment,
                settings.Backing().hero_mortality_factor(),
                settings.Backing().base_deadliness());
    }
}

auto ReserveUnitsForPlayer(const Units *units, const PlayerId pid)
        -> std::map<UnitId, const Unit *> {
    // Putting these into a std::map to have them sorted by unit id
    std::map<UnitId, const Unit *> reserves{};
    for (const Unit *unit : *units) {
        if (unit->player_id() == pid &&
            unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
            reserves[unit->unit_id()] = unit;
    }

    return reserves;
}

auto PlacedUnitsForPlayer(const Units *units, const PlayerId pid)
        -> std::map<UnitId, const Unit *> {
    // Putting these into a std::map to have them sorted by unit id
    std::map<UnitId, const Unit *> placedUnits{};
    for (const Unit *unit : *units) {
        if (unit->player_id() == pid &&
            unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
            placedUnits[unit->unit_id()] = unit;
    }

    return placedUnits;
}

}  // namespace shardok
