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

#include "CombatUtils.hpp"

#include <cmath>

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

namespace shardok {

inline auto factorIntoAdjustment(
        const std::optional<double> &factorPercentile,
        const double swingPercentile) -> double {
    return 1.0 + (factorPercentile.has_value()
                          ? (*factorPercentile * swingPercentile) / (100.0 * 100.0)
                          : 0.0);
}

auto CombatUtils::CalculateHeroDamageGiven(
        const HeroFb &hero,
        const double percentileRoll,
        const SettingsGetter &settings,
        const bool isDuel) -> CombatDamage {
    double heroDamageAmt = NAN;
    const bool canFight = (hero.vigor() >= settings.Backing().minimum_vigor_for_combat());

    CombatDamage::Builder heroDamageBuilder = CombatDamage::Builder();

    if (canFight || isDuel) {
        if (hero.profession_info().profession() ==
            net::eagle0::shardok::storage::fb::Profession_MAGE) {
            heroDamageAmt = std::max(
                    0.0,
                    hero.wisdom() * settings.Backing().hero_damage_factor() *
                            (50.0 + percentileRoll) / 100.0);
            heroDamageBuilder.SetLightning(heroDamageAmt);
            heroDamageBuilder.SetCold(heroDamageAmt);
            heroDamageBuilder.SetFire(heroDamageAmt);
            heroDamageBuilder.SetAEssence(heroDamageAmt);
        } else {
            heroDamageAmt = std::max(
                    0.0,
                    hero.strength() * settings.Backing().hero_damage_factor() *
                            (50.0 + percentileRoll) / 100.0);
            heroDamageBuilder.SetSlashing(heroDamageAmt);
            heroDamageBuilder.SetCrushing(heroDamageAmt);
            heroDamageBuilder.SetStabbing(heroDamageAmt);
            heroDamageBuilder.SetPuncturing(heroDamageAmt);
        }
    }

    return heroDamageBuilder.Build();
}

auto CombatUtils::CalculateBattalionDamageGiven(
        const BattalionFb &battalion,
        const std::optional<double> &cmdrCharisma,
        const std::optional<double> &cmdrBravery,
        double percentileRoll,
        const SettingsGetter &settings) -> CombatDamage {
    CombatDamage::Builder cdBuilder = CombatDamage::Builder();

    const BattalionTypeSPtr &type = settings.GetBattalionType(battalion.type());

    const double randomnessAdjustment = factorIntoAdjustment(percentileRoll, type->randomnessSwing);
    const double moraleAdjustment = factorIntoAdjustment(battalion.morale(), type->moraleSwing);
    const double charismaAdjustment = factorIntoAdjustment(cmdrCharisma, type->charismaSwing);
    const double braveryAdjustment = factorIntoAdjustment(cmdrBravery, type->braverySwing);

    const double totalSwing =
            randomnessAdjustment * moraleAdjustment * charismaAdjustment * braveryAdjustment;

    for (int i = 0; i < (int)NUM_DAMAGE_TYPES; i++) {
        double damagePerTroop =
                (type->baseMeleeDamageByType[i] +
                 (battalion.training() / 100.0) * type->maxAugmentMeleeDamageByType[i]);
        double penetratingDamagePerTroop =
                (type->basePenetratingMeleeDamageByType[i] +
                 ((battalion.training() / 100.0) *
                  type->maxAugmentPenetratingMeleeDamageByType[i]));
        damagePerTroop *= totalSwing;
        penetratingDamagePerTroop *= totalSwing;

        if (damagePerTroop < 0.0) damagePerTroop = 0.0;

        cdBuilder.SetDamageByType((DamageType)i, false, damagePerTroop * battalion.size());
        cdBuilder.SetDamageByType(
                (DamageType)i,
                true,
                penetratingDamagePerTroop * battalion.size());
    }

    return cdBuilder.Build();
}

auto CombatUtils::CalculateUnitDamageGiven(
        const Unit *unit,
        const double heroRoll,
        const double battalionRoll,
        const SettingsGetter &settings) -> CombatDamage {
    const bool hasHero = unit->has_attached_hero();
    CombatDamage battalionDamage = CalculateBattalionDamageGiven(
            unit->battalion(),
            (hasHero ? unit->attached_hero().charisma() : std::optional<double>()),
            (hasHero ? unit->attached_hero().bravery() : std::optional<double>()),
            battalionRoll,
            settings);

    return (hasHero ? battalionDamage +
                              CalculateHeroDamageGiven(unit->attached_hero(), heroRoll, settings)
                    : battalionDamage);
}

auto CombatUtils::CalculateUnitChargeDamageGiven(
        const Unit *unit,
        const double heroRoll,
        const double battalionRoll,
        const SettingsGetter &settings) -> CombatDamage {
    const BattalionTypeSPtr &type = settings.GetBattalionType(unit->battalion().type());

    return type->chargeMultiplier *
           CalculateUnitDamageGiven(unit, heroRoll, battalionRoll, settings);
}

auto CombatUtils::CalculateUnitArcheryDamageGiven(
        const Unit &unit,
        double battalionRoll,
        const SettingsGetter &settings) -> CombatDamage {
    CombatDamage::Builder battCdBuilder = CombatDamage::Builder();

    const auto &hero = unit.attached_hero();
    const auto &battalion = unit.battalion();
    const auto &type = settings.GetBattalionType(battalion.type());

    const double randomnessAdjustment = factorIntoAdjustment(battalionRoll, type->randomnessSwing);
    const double moraleAdjustment = factorIntoAdjustment(battalion.morale(), type->moraleSwing);
    const double charismaAdjustment = factorIntoAdjustment(hero.charisma(), type->charismaSwing);

    const double totalSwing = randomnessAdjustment * moraleAdjustment * charismaAdjustment;

    for (int i = 0; i < (int)NUM_DAMAGE_TYPES; i++) {
        double damagePerTroop =
                type->baseMissileDamageByType[i] +
                (battalion.training() / 100.0) * type->maxAugmentMissileDamageByType[i];
        double penetratingDamagePerTroop =
                type->basePenetratingMissileDamageByType[i] +
                (battalion.training() / 100.0) * type->maxAugmentPenetratingMissileDamageByType[i];
        damagePerTroop *= totalSwing;
        penetratingDamagePerTroop *= totalSwing;

        if (damagePerTroop < 0.0) damagePerTroop = 0.0;

        battCdBuilder.SetDamageByType((DamageType)i, false, damagePerTroop * battalion.size());
        battCdBuilder.SetDamageByType(
                (DamageType)i,
                true,
                penetratingDamagePerTroop * battalion.size());
    }

    const CombatDamage heroDamage =
            CombatDamage::Builder()
                    .SetPuncturing(hero.agility() * settings.Backing().hero_damage_factor())
                    .Build();

    return heroDamage + battCdBuilder.Build();
}

auto CombatUtils::InternalPerformMelee(
        const ActionCost cost,
        const Unit *att,
        const Unit *def,
        const Terrain defenderTerrain,
        const AttackOrientation orientation,
        const double attackerRoll,
        const double defenderRoll,
        const bool isCharge,
        const bool isAmbush,
        const SettingsGetter &settings) -> MeleeResultFb {
    MeleeResultFb result{};
    result.attackerAfter = *att;
    result.defenderAfter = *def;

    Unit &attackerAfter = result.attackerAfter;
    Unit &defenderAfter = result.defenderAfter;

    MutatingSpendActionPoints(&attackerAfter, cost);

    double damageReceivedMultiplier =
            (def->stun_rounds_remaining() > 0
                     ? settings.Backing().damage_received_multiplier_from_stun()
                     : 1.0);
    CombatDamage attackerDamage =
            damageReceivedMultiplier * (isCharge ? CalculateUnitChargeDamageGiven(
                                                           &attackerAfter,
                                                           attackerRoll,
                                                           attackerRoll,
                                                           settings)
                                                 : CalculateUnitDamageGiven(
                                                           &attackerAfter,
                                                           attackerRoll,
                                                           attackerRoll,
                                                           settings));

    double damageGivenMultiplier =
            (def->stun_rounds_remaining() > 0
                     ? settings.Backing().damage_given_multiplier_from_stun()
                     : 1.0);
    CombatDamage defenderDamage =
            damageGivenMultiplier *
            CalculateUnitDamageGiven(def, defenderRoll, defenderRoll, settings);

    attackerAfter =
            (isAmbush ? attackerAfter
                      : WithTakenDamageFb(
                                &attackerAfter,
                                defenderDamage,
                                Terrain(net::eagle0::shardok::storage::fb::Terrain_::Type_NONE,
                                        net::eagle0::shardok::storage::fb::TileModifier()),
                                front,
                                false,
                                false,
                                settings));

    defenderAfter = WithTakenDamageFb(
            def,
            attackerDamage,
            defenderTerrain,
            orientation,
            isAmbush,
            true,
            settings);

    attackerAfter.mutate_hidden(false);

    // Check for stun on charge
    if (isCharge) {
        const BattalionTypeSPtr &attackerBattalionType =
                settings.GetBattalionType(att->battalion().type());
        double attackerTraining = att->battalion().training();
        double defenderTraining = def->battalion().training();

        PercentileRollOdds stunOdds =
                attackerBattalionType->GetStunChanceOnCharge(attackerTraining, defenderTraining);
        double roll = 100.0 - attackerRoll;
        if (PercentileRollSucceeds(stunOdds, roll)) {
            defenderAfter.mutate_stun_rounds_remaining(def->stun_rounds_remaining() + 1);
        }
    }

    // Check for stun on ambush
    if (isAmbush) { defenderAfter.mutate_stun_rounds_remaining(def->stun_rounds_remaining() + 1); }

    if (att->player_id() != UNCONTROLLED_PLAYER_ID && def->player_id() != UNCONTROLLED_PLAYER_ID) {
        MutatingBumpOpponentKnowledge(
                &attackerAfter,
                defenderAfter.player_id(),
                settings.Backing().melee_knowledge_increment());
        MutatingBumpOpponentKnowledge(
                &defenderAfter,
                attackerAfter.player_id(),
                settings.Backing().melee_knowledge_increment());
    }

    int attackerCasualties = (att->battalion().size() - attackerAfter.battalion().size());
    int defenderCasualties = (def->battalion().size() - defenderAfter.battalion().size());

    int diff = attackerCasualties - defenderCasualties;

    double moraleShiftPerCasualty = settings.Backing().morale_shift_per_casualty();
    MutatingAdjustMorale(
            &attackerAfter,
            settings.GetBattalionType(attackerAfter.battalion().type()),
            -diff * moraleShiftPerCasualty);
    MutatingAdjustMorale(
            &defenderAfter,
            settings.GetBattalionType(defenderAfter.battalion().type()),
            diff * moraleShiftPerCasualty);

    int strengthXp = settings.Backing().melee_strength_xp();
    MutatingBumpStrengthXp(&attackerAfter, strengthXp);
    MutatingBumpStrengthXp(&defenderAfter, strengthXp);
    if (isAmbush) { MutatingBumpAgilityXp(&attackerAfter, settings.Backing().ambush_agility_xp()); }
    if (isCharge) { MutatingBumpAgilityXp(&attackerAfter, settings.Backing().charge_agility_xp()); }

    return result;
}

auto MakeActionResultFb(const ActionType type, const MeleeResultFb &results, const bool resetZoc)
        -> ActionResult {
    ActionResult ar{};
    ar.set_type(type);
    ar.mutable_player()->set_value(results.attackerAfter.player_id());
    ar.mutable_actor()->set_value(results.attackerAfter.unit_id());
    ar.mutable_target_unit()->set_value(results.defenderAfter.unit_id());

    AddChangedUnit(ar, results.attackerAfter);
    auto *attCU = (Unit *)ar.mutable_changed_units_fb(ar.changed_units_fb_size() - 1)->data();
    if (resetZoc) attCU->mutate_has_moved_in_zoc(false);

    AddChangedUnit(ar, results.defenderAfter);

    return ar;
}

auto CombatUtils::MeleeActionResult(const MeleeResultFb &results) -> ActionResult {
    return MakeActionResultFb(net::eagle0::shardok::common::MELEE_ATTACK, results, false);
}

auto CombatUtils::MeleeAmbushResult(const MeleeResultFb &results) -> ActionResult {
    return MakeActionResultFb(net::eagle0::shardok::common::MELEE_AMBUSH, results, false);
}

auto CombatUtils::ChargeResult(const MeleeResultFb &results) -> ActionResult {
    return MakeActionResultFb(net::eagle0::shardok::common::CHARGE_ATTACK, results, true);
}
}  // namespace shardok
