//
//  BattalionType.hpp
//  eagle0
//
//  Created by Dan Crosby on 11/25/14.
//  Copyright (c) 2014 none. All rights reserved.
//

#ifndef __eagle0__BattalionType__
#define __eagle0__BattalionType__

#include <cstdint>
#include <string>

#include "ActionCost.hpp"
#include "CombatDamage.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/PercentileRollOdds.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/Terrain.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/TileModifier.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/battalion_type.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/terrain.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/terrain.pb.h"

namespace shardok {
using Terrain = net::eagle0::shardok::storage::fb::Terrain;
using TerrainProto = net::eagle0::shardok::common::Terrain;
using BattalionTypeId = net::eagle0::shardok::storage::fb::BattalionTypeId;

struct BattalionType {
    const std::string name;
    const BattalionTypeId typeId;

    const int32_t capacity;

    const double randomnessSwing;
    const double moraleSwing;
    const double charismaSwing;
    const double braverySwing;

    const double flankDamageMultiplier;  // Represents multiplier of damage TAKEN when flanked
    const double rearDamageMultiplier;   // Represents multiplier of damage TAKEN when rear-attacked

    const double chargeMultiplier;  // Multiplier of damage GIVEN when charging

    const bool allowsCasting;
    const bool allowsStealth;
    const bool allowsBraveWater;
    const bool allowsMoveAfterCharge;

    const bool adjustsMorale;

    const bool alwaysHiddenInWater;

    const ActionCost costToEnterPlains;
    const ActionCost costToEnterHill;
    const ActionCost costToEnterForest;
    const ActionCost costToEnterMountain;
    const ActionCost costToEnterSwamp;
    const ActionCost costToEnterCity;
    const ActionCost costToEnterWater;

    const ActionCost costToEnterIce;
    const uint8_t minimumCostToEnterBridge;
    const ActionCost costToEnterCastle;

    const double snowPenalty;

    const double damageTakenMultiplierForPlains;
    const double damageTakenMultiplierForHill;
    const double damageTakenMultiplierForForest;
    const double damageTakenMultiplierForMountain;
    const double damageTakenMultiplierForSwamp;
    const double damageTakenMultiplierForCity;
    const double damageTakenMultiplierForWater;
    const double damageTakenMultiplierForCastle;

    const double baseMeleeDamageByType[NUM_DAMAGE_TYPES];
    const double maxAugmentMeleeDamageByType[NUM_DAMAGE_TYPES];

    const double basePenetratingMeleeDamageByType[NUM_DAMAGE_TYPES];
    const double maxAugmentPenetratingMeleeDamageByType[NUM_DAMAGE_TYPES];

    const double baseMissileDamageByType[NUM_DAMAGE_TYPES];
    const double maxAugmentMissileDamageByType[NUM_DAMAGE_TYPES];

    const double basePenetratingMissileDamageByType[NUM_DAMAGE_TYPES];
    const double maxAugmentPenetratingMissileDamageByType[NUM_DAMAGE_TYPES];

    const double baseResistanceByType[NUM_DAMAGE_TYPES];
    const double maxAugmentResistanceByType[NUM_DAMAGE_TYPES];

    const bool alwaysArcheryCapable;
    const int bonusVolleys;

    const double ambushOddsWithoutHero;

    const double baseStunChanceOnCharge;
    const double trainingDifferenceMultiplierForStunChanceOnCharge;

    const double monthlyFoodCostPerTroop;

    [[nodiscard]] auto GetDamageTakenMultiplierForTerrain(const TerrainProto &terrain) const
            -> double {
        if (terrain.type() == net::eagle0::shardok::common::Terrain_Type_NONE) return 1.0;
        const double terrainTypeMultiplier = GetDamageTakenMultiplierForTerrainType(terrain.type());

        if (HasCastle(terrain.modifier())) {
            const double castleIntegrity = terrain.modifier().castle().integrity().value();
            return (castleIntegrity * damageTakenMultiplierForCastle +
                    (100.0 - castleIntegrity) * terrainTypeMultiplier) /
                   100.0;
        }
        return terrainTypeMultiplier;
    }

    [[nodiscard]] auto GetDamageTakenMultiplierForTerrain(const Terrain *terrain) const -> double {
        if (terrain->type() == net::eagle0::shardok::storage::fb::Terrain_::Type_NONE) return 1.0;
        const double terrainTypeMultiplier =
                GetDamageTakenMultiplierForTerrainType(terrain->type());

        if (terrain->modifier().castle().present()) {
            const double castleIntegrity = terrain->modifier().castle().integrity();
            return (castleIntegrity * damageTakenMultiplierForCastle +
                    (100.0 - castleIntegrity) * terrainTypeMultiplier) /
                   100.0;
        }
        return terrainTypeMultiplier;
    }

    [[nodiscard]] auto GetDamageTakenMultiplierForTerrainType(
            const TerrainProto::Type terrainType) const -> double {
        switch (terrainType) {
            case net::eagle0::shardok::common::Terrain_Type_UNKNOWN:
            case net::eagle0::shardok::common::
                    Terrain_Type_Terrain_Type_INT_MIN_SENTINEL_DO_NOT_USE_:
            case net::eagle0::shardok::common::
                    Terrain_Type_Terrain_Type_INT_MAX_SENTINEL_DO_NOT_USE_:
                throw ShardokInternalErrorException("Unknown terrain type");
            case net::eagle0::shardok::common::Terrain_Type_NONE: return 1.0;
            case net::eagle0::shardok::common::Terrain_Type_PLAINS:
                return damageTakenMultiplierForPlains;
            case net::eagle0::shardok::common::Terrain_Type_HILL:
                return damageTakenMultiplierForHill;
            case net::eagle0::shardok::common::Terrain_Type_FOREST:
                return damageTakenMultiplierForForest;
            case net::eagle0::shardok::common::Terrain_Type_MOUNTAIN:
                return damageTakenMultiplierForMountain;
            case net::eagle0::shardok::common::Terrain_Type_SWAMP:
                return damageTakenMultiplierForSwamp;
            case net::eagle0::shardok::common::Terrain_Type_CITY:
                return damageTakenMultiplierForCity;
            case net::eagle0::shardok::common::Terrain_Type_STILL_WATER:
            case net::eagle0::shardok::common::Terrain_Type_RIVER:
                return damageTakenMultiplierForWater;
        }

        throw ShardokInternalErrorException("Unknown terrain type");
    }

    [[nodiscard]] auto GetDamageTakenMultiplierForTerrainType(
            const net::eagle0::shardok::storage::fb::Terrain_::Type terrainType) const -> double {
        switch (terrainType) {
            case net::eagle0::shardok::storage::fb::Terrain_::Type_UNKNOWN:
                throw ShardokInternalErrorException("Unknown terrain type");
            case net::eagle0::shardok::storage::fb::Terrain_::Type_NONE: return 1.0;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_PLAINS:
                return damageTakenMultiplierForPlains;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_HILL:
                return damageTakenMultiplierForHill;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST:
                return damageTakenMultiplierForForest;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN:
                return damageTakenMultiplierForMountain;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_SWAMP:
                return damageTakenMultiplierForSwamp;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_CITY:
                return damageTakenMultiplierForCity;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_STILL_WATER:
            case net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER:
                return damageTakenMultiplierForWater;
        }

        throw ShardokInternalErrorException("Unknown terrain type");
    }

    [[nodiscard]] auto GetCostToEnterTerrain(const TerrainProto &terrain) const -> ActionCost {
        if (HasBridge(terrain.modifier()))
            return ActionCost(
                    ActionCost::standard,
                    ActionPoints(std::ceil(
                            100.0 * minimumCostToEnterBridge /
                            terrain.modifier().bridge().integrity().value())));
        if (IsFrozen(terrain.modifier())) return costToEnterIce;

        auto terrainCost = GetCostToEnterTerrainType(terrain.type());
        if (HasSnow(terrain.modifier())) {
            terrainCost.points += int(snowPenalty * terrain.modifier().snow().integrity().value());
        }
        return terrainCost;
    }

    [[nodiscard]] auto GetCostToEnterTerrain(
            const net::eagle0::shardok::storage::fb::Terrain_::Type terrainType,
            const net::eagle0::shardok::storage::fb::TileModifier &tm) const -> ActionCost {
        if (tm.bridge().present())
            return ActionCost(
                    ActionCost::standard,
                    ActionPoints(
                            std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity())));
        if (tm.ice().present()) return costToEnterIce;

        auto terrainCost = GetCostToEnterTerrainType(terrainType);
        if (tm.snow().present()) { terrainCost.points += int(snowPenalty * tm.snow().integrity()); }
        return terrainCost;
    }

    [[nodiscard]] auto GetCostToEnterTerrain(const Terrain *terrain) const -> ActionCost {
        return GetCostToEnterTerrain(terrain->type(), terrain->modifier());
    }

    [[nodiscard]] auto GetCostToEnterTerrainType(const TerrainProto::Type terrainType) const
            -> ActionCost {
        switch (terrainType) {
            case net::eagle0::shardok::common::Terrain_Type_UNKNOWN:
            case net::eagle0::shardok::common::
                    Terrain_Type_Terrain_Type_INT_MIN_SENTINEL_DO_NOT_USE_:
            case net::eagle0::shardok::common::
                    Terrain_Type_Terrain_Type_INT_MAX_SENTINEL_DO_NOT_USE_:
                throw ShardokInternalErrorException("Unknown terrain type");
            case net::eagle0::shardok::common::Terrain_Type_NONE: return IMPOSSIBLE_ACTION_COST;
            case net::eagle0::shardok::common::Terrain_Type_PLAINS: return costToEnterPlains;
            case net::eagle0::shardok::common::Terrain_Type_HILL: return costToEnterHill;
            case net::eagle0::shardok::common::Terrain_Type_FOREST: return costToEnterForest;
            case net::eagle0::shardok::common::Terrain_Type_MOUNTAIN: return costToEnterMountain;
            case net::eagle0::shardok::common::Terrain_Type_SWAMP: return costToEnterSwamp;
            case net::eagle0::shardok::common::Terrain_Type_CITY: return costToEnterCity;
            case net::eagle0::shardok::common::Terrain_Type_STILL_WATER:
            case net::eagle0::shardok::common::Terrain_Type_RIVER: return costToEnterWater;
        }

        throw ShardokInternalErrorException("Unknown terrain type");
    }

    [[nodiscard]] auto GetCostToEnterTerrainType(
            const net::eagle0::shardok::storage::fb::Terrain_::Type terrainType) const
            -> ActionCost {
        switch (terrainType) {
            case net::eagle0::shardok::storage::fb::Terrain_::Type_UNKNOWN:
                throw ShardokInternalErrorException("Unknown terrain type");
            case net::eagle0::shardok::storage::fb::Terrain_::Type_NONE:
                return IMPOSSIBLE_ACTION_COST;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_PLAINS: return costToEnterPlains;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_HILL: return costToEnterHill;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST: return costToEnterForest;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN:
                return costToEnterMountain;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_SWAMP: return costToEnterSwamp;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_CITY: return costToEnterCity;
            case net::eagle0::shardok::storage::fb::Terrain_::Type_STILL_WATER:
            case net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER: return costToEnterWater;
        }

        throw ShardokInternalErrorException("Unknown terrain type");
    }

    [[nodiscard]] auto AlwaysHiddenInTerrain(const Terrain *terrain) const -> bool {
        return alwaysHiddenInWater && IsWater(terrain->type()) &&
               !terrain->modifier().ice().present();
    }

    [[nodiscard]] auto AlwaysHiddenInTerrain(const TerrainProto &terrain) const -> bool {
        return alwaysHiddenInWater && IsWater(terrain.type()) && !IsFrozen(terrain.modifier());
    }

    [[nodiscard]] auto GetStunChanceOnCharge(
            const double chargerTraining,
            const double defenderTraining) const -> PercentileRollOdds {
        const double trainingDifference = chargerTraining - defenderTraining;
        const double trainingValue =
                trainingDifferenceMultiplierForStunChanceOnCharge * trainingDifference;
        OtherFactor trainingFactor = MakeOtherFactor((int16_t)trainingValue, "training difference");
        return MakeOdds((int16_t)baseStunChanceOnCharge, 0, 0, 0, {}, {trainingFactor});
    }

    [[nodiscard]] auto GetDailyFoodCostPerTroop() const -> double {
        return monthlyFoodCostPerTroop / 31.0;
    }

    static auto NewBattalionTypeFromMap(const std::unordered_map<std::string, std::string> &map)
            -> std::shared_ptr<const BattalionType>;
};

typedef std::shared_ptr<const BattalionType> BattalionTypeSPtr;
}  // namespace shardok

#endif /* defined(__eagle0__BattalionType__) */
