using PkmnLib.Dynamic.Libraries;
using PkmnLib.Static.Moves;

namespace PkmnLib.Plugin.Gen7.Libraries.Battling;

public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDamageCalculator
{
    /// <inheritdoc />
    public uint GetDamage(IExecutingMove? executingMove, MoveCategory category, IPokemon user, IPokemon target,
        int targetCount, byte hitNumber, IHitData hitData)
    {
        if (category == MoveCategory.Status)
            return 0;
        if (hitData.Effectiveness == 0)
            return 0;

        var levelModifier = 2.0f * user.Level / 5.0f + 2.0f;
        var basePower = (float)hitData.BasePower;
        var statModifier = GetStatModifier(executingMove, category, user, target, hitNumber, hitData);
        var damageModifier = executingMove == null ? 1.0f : GetDamageModifier(executingMove, target, hitNumber);

        var floatDamage = MathF.Floor(levelModifier * basePower);
        floatDamage = MathF.Floor(floatDamage * statModifier);
        floatDamage = MathF.Floor(floatDamage / 50.0f) + 2.0f;
        floatDamage = MathF.Floor(floatDamage * damageModifier);
        if (targetCount > 1)
            floatDamage = MathF.Floor(floatDamage * 0.75f);

        if (hitData.IsCritical)
        {
            var critModifier = 1.5f;
            executingMove?.RunScriptHook<IScriptChangeCriticalModifier>(script =>
                script.ChangeCriticalModifier(executingMove, target, hitNumber, ref critModifier));
            floatDamage = MathF.Floor(floatDamage * critModifier);
        }

        if (configuration.DamageCalculatorHasRandomness)
        {
            var battle = target.BattleData?.Battle;
            if (battle == null)
                throw new InvalidOperationException("Randomness is enabled, but no battle is set.");
            var random = battle.Random;
            var randomFactor = random.GetInt(85, 101) / 100.0f;
            floatDamage = MathF.Floor(floatDamage * randomFactor);
        }

        var stabModifier = 1.0f;
        var isStab = false;
        if (hitData.Type != null && user.Types.Contains(hitData.Type.Value))
        {
            stabModifier = 1.5f;
            isStab = true;
        }
        executingMove?.RunScriptHook<IScriptChangeStabModifier>(script =>
            script.ChangeStabModifier(executingMove, target, hitNumber, isStab, ref stabModifier));
        floatDamage = MathF.Floor(floatDamage * stabModifier);

        floatDamage = MathF.Floor(floatDamage * hitData.Effectiveness);
        uint damage = floatDamage switch
        {
            > uint.MaxValue => uint.MaxValue,
            < 1 => 1,
            _ => (uint)floatDamage,
        };

        if (executingMove is not null)
        {
            executingMove.RunScriptHook<IScriptChangeMoveDamage>(script =>
                script.ChangeMoveDamage(executingMove, target, hitNumber, ref damage));
            target.RunScriptHook<IScriptChangeIncomingMoveDamage>(script =>
                script.ChangeIncomingMoveDamage(executingMove, target, hitNumber, ref damage));
        }

        return damage;
    }

    /// <inheritdoc />
    public ushort GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData)
    {
        if (executingMove.UseMove.Category == MoveCategory.Status)
            return 0;
        var basePower = (ushort)executingMove.UseMove.BasePower;
        executingMove.RunScriptHook<IScriptChangeBasePower>(script =>
            script.ChangeBasePower(executingMove, target, hitNumber, ref basePower));
        return basePower;
    }

    /// <inheritdoc />
    public bool IsCritical(IBattle battle, IExecutingMove executingMove, IPokemon target, byte hitNumber)
    {
        if (executingMove.UseMove.Category == MoveCategory.Status)
            return false;
        byte critStage = 0;
        executingMove.RunScriptHook<IScriptChangeCriticalStage>(script =>
            script.ChangeCriticalStage(executingMove, target, hitNumber, ref critStage));

        var random = battle.Random;
        return critStage switch
        {
            0 => random.GetInt(24) == 0,
            1 => random.GetInt(8) == 0,
            2 => random.GetInt(2) == 0,
            _ => true,
        };
    }

    private static float GetStatModifier(IExecutingMove? executingMove, MoveCategory category, IPokemon user,
        IPokemon target, byte hitNumber, IHitData hitData)
    {
        if (category == MoveCategory.Status)
            return 1;

        var (offensive, defensive) = category switch
        {
            MoveCategory.Physical => (Statistic.Attack, Statistic.Defense),
            _ => (Statistic.SpecialAttack, Statistic.SpecialDefense),
        };

        // Check if we can bypass the defensive stat boost on the target. We default to this if the
        // move is critical, and the target has a defensive stat boost of > 0, but a script is
        // allowed to change this.
        var bypassDefense = hitData.IsCritical && target.StatBoost.GetStatistic(defensive) > 0;
        executingMove?.RunScriptHook<IScriptBypassDefensiveStatBoosts>(script =>
            script.BypassDefensiveStatBoosts(executingMove, target, hitNumber, ref bypassDefense));

        // Check if we can bypass the offensive stat boost on the user. We default to this if the
        // move is critical, and the user has an offensive stat boost of < 0, but a script is
        // allowed to change this.
        var bypassOffense = hitData.IsCritical && user.StatBoost.GetStatistic(offensive) < 0;
        executingMove?.RunScriptHook<IScriptBypassOffensiveStatBoosts>(script =>
            script.BypassOffensiveStatBoosts(executingMove, target, hitNumber, ref bypassOffense));

        var userStats = user.BoostedStats;
        if (bypassOffense)
            userStats = user.FlatStats;
        var offensiveStat = userStats.GetStatistic(offensive);

        var targetStats = target.BoostedStats;
        if (bypassDefense)
            targetStats = target.FlatStats;
        var defensiveStat = targetStats.GetStatistic(defensive);
        var origOffensiveStat = offensiveStat;

        if (executingMove != null)
        {
            executingMove.RunScriptHook<IScriptChangeOffensiveStatValue>(script =>
                script.ChangeOffensiveStatValue(executingMove, target, hitNumber, defensiveStat, targetStats, offensive,
                    ref offensiveStat));
            executingMove.RunScriptHook<IScriptChangeDefensiveStatValue>(script =>
                script.ChangeDefensiveStatValue(executingMove, target, hitNumber, origOffensiveStat, targetStats,
                    defensive, ref defensiveStat));
            target.RunScriptHook<IScriptChangeIncomingMoveOffensiveStatValue>(script =>
                script.ChangeIncomingMoveOffensiveStatValue(executingMove, target, hitNumber, defensiveStat,
                    targetStats, offensive, ref offensiveStat));
            target.RunScriptHook<IScriptChangeIncomingMoveDefensiveStatValue>(script =>
                script.ChangeIncomingMoveDefensiveStatValue(executingMove, target, hitNumber, origOffensiveStat,
                    targetStats, defensive, ref defensiveStat));
        }

        var modifier = (float)offensiveStat / defensiveStat;
        executingMove?.RunScriptHook<IScriptChangeDamageStatModifier>(script =>
            script.ChangeDamageStatModifier(executingMove, target, hitNumber, ref modifier));

        return modifier;
    }

    /// <summary>
    /// Gets the damage modifier. This is a value that defaults to 1.0, but can be modified by scripts
    /// to apply a raw modifier to the damage.
    /// </summary>
    private static float GetDamageModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber)
    {
        var modifier = 1.0f;

        executingMove.RunScriptHook<IScriptChangeDamageModifier>(script =>
            script.ChangeDamageModifier(executingMove, target, hitNumber, ref modifier));
        target.RunScriptHook<IScriptChangeIncomingMoveDamageModifier>(script =>
            script.ChangeIncomingMoveDamageModifier(executingMove, target, hitNumber, ref modifier));

        return modifier;
    }
}