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

#ifndef EAGLE0_SHARDOK_LIBRARY_COMBAT_DAMAGE_HPP
#define EAGLE0_SHARDOK_LIBRARY_COMBAT_DAMAGE_HPP

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <sstream>
#include <string>
#include <vector>

namespace shardok {
// Normal and penetrating damage for each kind
enum class DamageType : std::uint8_t {
    DamageType_slashing = 0,
    DamageType_puncturing,
    DamageType_stabbing,
    DamageType_crushing,
    DamageType_fire,
    DamageType_cold,
    DamageType_lightning,
    DamageType_aessence,
    DamageType_impact,
    NUM_DAMAGE_TYPES
};

inline constexpr DamageType DamageType_slashing = DamageType::DamageType_slashing;
inline constexpr DamageType DamageType_puncturing = DamageType::DamageType_puncturing;
inline constexpr DamageType DamageType_stabbing = DamageType::DamageType_stabbing;
inline constexpr DamageType DamageType_crushing = DamageType::DamageType_crushing;
inline constexpr DamageType DamageType_fire = DamageType::DamageType_fire;
inline constexpr DamageType DamageType_cold = DamageType::DamageType_cold;
inline constexpr DamageType DamageType_lightning = DamageType::DamageType_lightning;
inline constexpr DamageType DamageType_aessence = DamageType::DamageType_aessence;
inline constexpr DamageType DamageType_impact = DamageType::DamageType_impact;
inline constexpr int NUM_DAMAGE_TYPES = static_cast<int>(DamageType::NUM_DAMAGE_TYPES);

[[nodiscard]] static inline bool DamageTypeGetsTerrainDefense(const DamageType type) {
    switch (type) {
        case DamageType_slashing:
        case DamageType_puncturing:
        case DamageType_stabbing:
        case DamageType_crushing:
        case DamageType_impact: return true;

        case DamageType_fire:
        case DamageType_cold:
        case DamageType_lightning:
        case DamageType_aessence: return false;

        default: return true;
    }

    return false;
}

struct CombatDamage {
private:
    static constexpr size_t kDamageValueCount = 2 * static_cast<size_t>(NUM_DAMAGE_TYPES);

    std::array<double, kDamageValueCount> damageByType{};

    [[nodiscard]] static constexpr auto NormalIndex(const DamageType type) -> size_t {
        return 2 * static_cast<size_t>(type);
    }

    [[nodiscard]] static constexpr auto PenetratingIndex(const DamageType type) -> size_t {
        return NormalIndex(type) + 1;
    }

public:
    CombatDamage() = default;
    CombatDamage(
            const double sl,
            const double pu,
            const double st,
            const double cr,
            const double fi,
            const double co,
            const double li,
            const double aes,
            const double im,
            const double psl,
            const double ppu,
            const double pst,
            const double pcr,
            const double pfi,
            const double pco,
            const double pli,
            const double paes,
            const double pim)
        : damageByType{
                  sl,
                  psl,
                  pu,
                  ppu,
                  st,
                  pst,
                  cr,
                  pcr,
                  fi,
                  pfi,
                  co,
                  pco,
                  li,
                  pli,
                  aes,
                  paes,
                  im,
                  pim} {}

    explicit CombatDamage(const std::vector<double>& byType) {
        std::copy_n(
                byType.begin(),
                std::min(byType.size(), damageByType.size()),
                damageByType.begin());
    }

    [[nodiscard]] auto GetNormalDamageOfType(const DamageType type) const -> double {
        return damageByType.at(NormalIndex(type));
    }
    [[nodiscard]] auto GetPenetratingDamageOfType(const DamageType type) const -> double {
        return damageByType.at(PenetratingIndex(type));
    };

    [[nodiscard]] bool operator==(const CombatDamage& cmpr) const {
        return damageByType == cmpr.damageByType;
    }

    [[nodiscard]] bool operator!=(const CombatDamage& cmpr) const { return !(*this == cmpr); }

    [[nodiscard]] bool operator<=(const CombatDamage& cmpr) const {
        return std::ranges::equal(
                damageByType,
                cmpr.damageByType,
                [](const double damage, const double comparedDamage) {
                    return damage <= comparedDamage;
                });
    }

    [[nodiscard]] bool operator>=(const CombatDamage& cmpr) const {
        return std::ranges::equal(
                damageByType,
                cmpr.damageByType,
                [](const double damage, const double comparedDamage) {
                    return damage >= comparedDamage;
                });
    }

    [[nodiscard]] bool operator>(const CombatDamage& cmpr) const {
        return ((*this >= cmpr) && !(*this == cmpr));
    }

    [[nodiscard]] bool operator<(const CombatDamage& cmpr) const {
        return ((*this <= cmpr) && !(*this == cmpr));
    }

    [[nodiscard]] CombatDamage operator+(const CombatDamage& add) const {
        return Builder()
                .SetSlashing(
                        GetNormalDamageOfType(DamageType_slashing) +
                        add.GetNormalDamageOfType(DamageType_slashing))
                .SetPuncturing(
                        GetNormalDamageOfType(DamageType_puncturing) +
                        add.GetNormalDamageOfType(DamageType_puncturing))
                .SetStabbing(
                        GetNormalDamageOfType(DamageType_stabbing) +
                        add.GetNormalDamageOfType(DamageType_stabbing))
                .SetCrushing(
                        GetNormalDamageOfType(DamageType_crushing) +
                        add.GetNormalDamageOfType(DamageType_crushing))
                .SetFire(
                        GetNormalDamageOfType(DamageType_fire) +
                        add.GetNormalDamageOfType(DamageType_fire))
                .SetCold(
                        GetNormalDamageOfType(DamageType_cold) +
                        add.GetNormalDamageOfType(DamageType_cold))
                .SetLightning(
                        GetNormalDamageOfType(DamageType_lightning) +
                        add.GetNormalDamageOfType(DamageType_lightning))
                .SetAEssence(
                        GetNormalDamageOfType(DamageType_aessence) +
                        add.GetNormalDamageOfType(DamageType_aessence))
                .SetImpact(
                        GetNormalDamageOfType(DamageType_impact) +
                        add.GetNormalDamageOfType(DamageType_impact))

                .SetPenetratingSlashing(
                        GetPenetratingDamageOfType(DamageType_slashing) +
                        add.GetPenetratingDamageOfType(DamageType_slashing))
                .SetPenetratingPuncturing(
                        GetPenetratingDamageOfType(DamageType_puncturing) +
                        add.GetPenetratingDamageOfType(DamageType_puncturing))
                .SetPenetratingStabbing(
                        GetPenetratingDamageOfType(DamageType_stabbing) +
                        add.GetPenetratingDamageOfType(DamageType_stabbing))
                .SetPenetratingCrushing(
                        GetPenetratingDamageOfType(DamageType_crushing) +
                        add.GetPenetratingDamageOfType(DamageType_crushing))
                .SetPenetratingFire(
                        GetPenetratingDamageOfType(DamageType_fire) +
                        add.GetPenetratingDamageOfType(DamageType_fire))
                .SetPenetratingCold(
                        GetPenetratingDamageOfType(DamageType_cold) +
                        add.GetPenetratingDamageOfType(DamageType_cold))
                .SetPenetratingLightning(
                        GetPenetratingDamageOfType(DamageType_lightning) +
                        add.GetPenetratingDamageOfType(DamageType_lightning))
                .SetPenetratingAEssence(
                        GetPenetratingDamageOfType(DamageType_aessence) +
                        add.GetPenetratingDamageOfType(DamageType_aessence))
                .SetPenetratingImpact(
                        GetPenetratingDamageOfType(DamageType_impact) +
                        add.GetPenetratingDamageOfType(DamageType_impact))

                .Build();
    }

    [[nodiscard]] CombatDamage operator*(const double factor) const {
        return Builder()
                .SetSlashing(GetNormalDamageOfType(DamageType_slashing) * factor)
                .SetPuncturing(GetNormalDamageOfType(DamageType_puncturing) * factor)
                .SetStabbing(GetNormalDamageOfType(DamageType_stabbing) * factor)
                .SetCrushing(GetNormalDamageOfType(DamageType_crushing) * factor)
                .SetFire(GetNormalDamageOfType(DamageType_fire) * factor)
                .SetCold(GetNormalDamageOfType(DamageType_cold) * factor)
                .SetLightning(GetNormalDamageOfType(DamageType_lightning) * factor)
                .SetAEssence(GetNormalDamageOfType(DamageType_aessence) * factor)
                .SetImpact(GetNormalDamageOfType(DamageType_impact) * factor)

                .SetPenetratingSlashing(GetPenetratingDamageOfType(DamageType_slashing) * factor)
                .SetPenetratingPuncturing(
                        GetPenetratingDamageOfType(DamageType_puncturing) * factor)
                .SetPenetratingStabbing(GetPenetratingDamageOfType(DamageType_stabbing) * factor)
                .SetPenetratingCrushing(GetPenetratingDamageOfType(DamageType_crushing) * factor)
                .SetPenetratingFire(GetPenetratingDamageOfType(DamageType_fire) * factor)
                .SetPenetratingCold(GetPenetratingDamageOfType(DamageType_cold) * factor)
                .SetPenetratingLightning(GetPenetratingDamageOfType(DamageType_lightning) * factor)
                .SetPenetratingAEssence(GetPenetratingDamageOfType(DamageType_aessence) * factor)
                .SetPenetratingImpact(GetPenetratingDamageOfType(DamageType_impact) * factor)
                .Build();
    }

    [[nodiscard]] auto ToString() const -> std::string {
        std::ostringstream out;
        out << std::fixed << std::setprecision(6)
            << "slashing=" << GetNormalDamageOfType(DamageType_slashing) << " / "
            << GetPenetratingDamageOfType(DamageType_slashing)
            << ", puncturing=" << GetNormalDamageOfType(DamageType_puncturing) << " / "
            << GetPenetratingDamageOfType(DamageType_puncturing)
            << ", stabbing=" << GetNormalDamageOfType(DamageType_stabbing) << " / "
            << GetPenetratingDamageOfType(DamageType_stabbing)
            << ", crushing=" << GetNormalDamageOfType(DamageType_crushing) << " / "
            << GetPenetratingDamageOfType(DamageType_crushing)
            << ", fire=" << GetNormalDamageOfType(DamageType_fire) << " / "
            << GetPenetratingDamageOfType(DamageType_fire)
            << ", cold=" << GetNormalDamageOfType(DamageType_cold) << " / "
            << GetPenetratingDamageOfType(DamageType_cold)
            << ", lightning=" << GetNormalDamageOfType(DamageType_lightning) << " / "
            << GetPenetratingDamageOfType(DamageType_lightning)
            << ", aessence=" << GetNormalDamageOfType(DamageType_aessence) << " / "
            << GetPenetratingDamageOfType(DamageType_aessence)
            << ", impact=" << GetNormalDamageOfType(DamageType_impact) << " / "
            << GetPenetratingDamageOfType(DamageType_impact);
        return out.str();
    }

    class Builder {
    private:
        double slashing = 0.0;
        double puncturing = 0.0;
        double stabbing = 0.0;
        double crushing = 0.0;
        double fire = 0.0;
        double cold = 0.0;
        double lightning = 0.0;
        double aessence = 0.0;
        double impact = 0.0;

        double penetrating_slashing = 0.0;
        double penetrating_puncturing = 0.0;
        double penetrating_stabbing = 0.0;
        double penetrating_crushing = 0.0;
        double penetrating_fire = 0.0;
        double penetrating_cold = 0.0;
        double penetrating_lightning = 0.0;
        double penetrating_aessence = 0.0;
        double penetrating_impact = 0.0;

    public:
        Builder() = default;
        Builder& SetSlashing(const double sl) {
            slashing = sl;
            return *this;
        }
        Builder& SetPuncturing(const double pu) {
            puncturing = pu;
            return *this;
        }
        Builder& SetStabbing(const double st) {
            stabbing = st;
            return *this;
        }
        Builder& SetCrushing(const double cr) {
            crushing = cr;
            return *this;
        }
        Builder& SetFire(const double fi) {
            fire = fi;
            return *this;
        }
        Builder& SetCold(const double co) {
            cold = co;
            return *this;
        }
        Builder& SetLightning(const double li) {
            lightning = li;
            return *this;
        }
        Builder& SetAEssence(const double aes) {
            aessence = aes;
            return *this;
        }
        Builder& SetImpact(const double im) {
            impact = im;
            return *this;
        }

        Builder& SetPenetratingSlashing(const double sl) {
            penetrating_slashing = sl;
            return *this;
        }
        Builder& SetPenetratingPuncturing(const double pu) {
            penetrating_puncturing = pu;
            return *this;
        }
        Builder& SetPenetratingStabbing(const double st) {
            penetrating_stabbing = st;
            return *this;
        }
        Builder& SetPenetratingCrushing(const double cr) {
            penetrating_crushing = cr;
            return *this;
        }
        Builder& SetPenetratingFire(const double fi) {
            penetrating_fire = fi;
            return *this;
        }
        Builder& SetPenetratingCold(const double co) {
            penetrating_cold = co;
            return *this;
        }
        Builder& SetPenetratingLightning(const double li) {
            penetrating_lightning = li;
            return *this;
        }
        Builder& SetPenetratingAEssence(const double aes) {
            penetrating_aessence = aes;
            return *this;
        }
        Builder& SetPenetratingImpact(const double im) {
            penetrating_impact = im;
            return *this;
        }

        Builder&
        SetDamageByType(const DamageType type, const bool isPenetrating, const double amt) {
            if (isPenetrating) {
                switch (type) {
                    case DamageType_slashing: SetPenetratingSlashing(amt); break;

                    case DamageType_puncturing: SetPenetratingPuncturing(amt); break;

                    case DamageType_stabbing: SetPenetratingStabbing(amt); break;

                    case DamageType_crushing: SetPenetratingCrushing(amt); break;

                    case DamageType_fire: SetPenetratingFire(amt); break;

                    case DamageType_cold: SetPenetratingCold(amt); break;

                    case DamageType_lightning: SetPenetratingLightning(amt); break;

                    case DamageType_aessence: SetPenetratingAEssence(amt); break;

                    case DamageType_impact: SetPenetratingImpact(amt); break;

                    default: throw 1;
                }

            } else {
                switch (type) {
                    case DamageType_slashing: SetSlashing(amt); break;

                    case DamageType_puncturing: SetPuncturing(amt); break;

                    case DamageType_stabbing: SetStabbing(amt); break;

                    case DamageType_crushing: SetCrushing(amt); break;

                    case DamageType_fire: SetFire(amt); break;

                    case DamageType_cold: SetCold(amt); break;

                    case DamageType_lightning: SetLightning(amt); break;

                    case DamageType_aessence: SetAEssence(amt); break;

                    case DamageType_impact: SetImpact(amt); break;

                    default: throw 1;
                }
            }

            return *this;
        }

        [[nodiscard]] auto Build() const -> CombatDamage {
            return {slashing,
                    puncturing,
                    stabbing,
                    crushing,
                    fire,
                    cold,
                    lightning,
                    aessence,
                    impact,
                    penetrating_slashing,
                    penetrating_puncturing,
                    penetrating_stabbing,
                    penetrating_crushing,
                    penetrating_fire,
                    penetrating_cold,
                    penetrating_lightning,
                    penetrating_aessence,
                    penetrating_impact};
        }
    };

    [[nodiscard]] auto ToBuilder() const -> Builder {
        Builder builder = Builder();
        for (int i = 0; i < static_cast<int>(NUM_DAMAGE_TYPES); ++i) {
            const auto type = static_cast<DamageType>(i);
            builder.SetDamageByType(type, false, this->GetNormalDamageOfType(type));
            builder.SetDamageByType(type, true, this->GetPenetratingDamageOfType(type));
        }
        return builder;
    }
};

[[nodiscard]] inline CombatDamage operator*(const double factor, const CombatDamage& dmg) {
    return dmg * factor;
}
}  // namespace shardok

#endif  // EAGLE0_SHARDOK_LIBRARY_COMBAT_DAMAGE_HPP
