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

namespace PkmnLib.Plugin.Gen7.Libraries;

public class Gen7DamageCalculator(bool hasRandomness) : IDamageCalculator
{
    /// <inheritdoc />
    public uint GetDamage(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData)
    {
        var category = executingMove.UseMove.Category;
        if (category == MoveCategory.Status)
            return 0;
        if (hitData.Effectiveness == 0)
            return 0;

        var levelModifier = (2.0f * executingMove.User.Level) / 5.0f + 2.0f;
        var basePower = (float)hitData.BasePower;
        var statModifier = GetStatModifier(executingMove, target, hitNumber, hitData);
        var damageModifier = 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 (executingMove.TargetCount > 1)
            floatDamage = MathF.Floor(floatDamage * 0.75f);

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

        if (hasRandomness)
        {
            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);
        }

        if (executingMove.User.Types.Contains(hitData.Type))
        {
            var stabModifier = 1.5f;
            executingMove.RunScriptHook(script =>
                script.ChangeStabModifier(executingMove, target, hitNumber, 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,
        };
        executingMove.RunScriptHook(script =>
            script.ChangeDamage(executingMove, target, hitNumber, ref damage));
        target.RunScriptHook(script =>
            script.ChangeIncomingDamage(executingMove, target, hitNumber, ref damage));

        return damage;
    }

    /// <inheritdoc />
    public byte GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData)
    {
        if (executingMove.UseMove.Category == MoveCategory.Status)
            return 0;
        var basePower = executingMove.UseMove.BasePower;
        executingMove.RunScriptHook(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(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, IPokemon target, byte hitNumber, IHitData hitData)
    {
        var category = executingMove.UseMove.Category;
        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(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 && executingMove.User.StatBoost.GetStatistic(offensive) < 0;
        executingMove.RunScriptHook(script =>
            script.BypassOffensiveStatBoosts(executingMove, target, hitNumber, ref bypassOffense));

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

        var targetStats = target.BoostedStats;
        if (bypassDefense)
            targetStats = target.FlatStats;
        var defensiveStat = targetStats.GetStatistic(defensive);
        
        executingMove.RunScriptHook(script =>
            script.ChangeOffensiveStatValue(executingMove, target, hitNumber, ref offensiveStat));
        executingMove.RunScriptHook(script =>
            script.ChangeDefensiveStatValue(executingMove, target, hitNumber, ref defensiveStat));

        var modifier = (float)offensiveStat / defensiveStat;
        executingMove.RunScriptHook(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(script =>
            script.ChangeDamageModifier(executingMove, target, hitNumber, ref modifier));

        return modifier;
    }
}