using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dynamic.Models.BattleFlow;

internal static class MoveTurnExecutor
{
    internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
    {
        var chosenMove = moveChoice.ChosenMove;
        var moveData = chosenMove.MoveData;
        
        var moveDataName = moveData.Name;
        moveChoice.RunScriptHook(x => x.ChangeMove(moveChoice, ref moveDataName));
        if (moveData.Name != moveDataName)
        {
            if (!battle.Library.StaticLibrary.Moves.TryGet(moveDataName, out moveData))
            {
                throw new InvalidOperationException(
                    $"The move was changed to '{moveDataName}' by a script, but this move does not exist.");
            }
            // FIXME: Change the script on the move when it is changed.
        }

        var targetType = moveData.Target;
        var targets = TargetResolver.ResolveTargets(battle, moveChoice.TargetSide, moveChoice.TargetPosition, targetType);
        
        byte numberOfHits = 1;
        moveChoice.RunScriptHook(x => x.ChangeNumberOfHits(moveChoice, ref numberOfHits));
        if (numberOfHits == 0)
        {
            return;
        }

        var executingMove = new ExecutingMoveImpl(targets, numberOfHits, moveChoice.User, chosenMove, moveData,
            moveChoice.Script);
        
        var prevented = false;
        executingMove.RunScriptHook(x => x.PreventMove(executingMove, ref prevented));
        if (prevented)
            return;

        byte ppUsed = 1;
        // TODO: Modify the PP used by the move.
        if (!executingMove.ChosenMove.TryUse(ppUsed))
            return;
        
        battle.EventHook.Invoke(new MoveUseEvent(executingMove));
        
        var failed = false;
        executingMove.RunScriptHook(x => x.FailMove(executingMove, ref failed));
        if (failed)
        {
            // TODO: fail handling
            return;
        }
        
        var stopped = false;
        executingMove.RunScriptHook(x => x.StopBeforeMove(executingMove, ref stopped));
        if (stopped)
            return;
        
        executingMove.RunScriptHook(x => x.OnBeforeMove(executingMove));
        foreach (var target in targets.WhereNotNull())
        {
            ExecuteMoveChoiceForTarget(battle, executingMove, target);
        }
    }

    private static void ExecuteMoveChoiceForTarget(IBattle battle, IExecutingMove executingMove, IPokemon target)
    {
        var failed = false;
        target.RunScriptHook(x => x.FailIncomingMove(executingMove, target, ref failed));
        if (failed)
        {
            // TODO: fail handling
            return;
        }
        
        var isInvulnerable = false;
        target.RunScriptHook(x => x.IsInvulnerableToMove(executingMove, target, ref isInvulnerable));
        if (isInvulnerable)
        {
            // TODO: event?
            return;
        }
        
        var numberOfHits = executingMove.NumberOfHits;
        var targetHitStat = executingMove.GetTargetIndex(target) * numberOfHits;

        for (byte i = 0; i < numberOfHits; i++)
        {
            if (battle.HasEnded)
                break;
            if (executingMove.User.IsFainted)
                break;
            if (target.IsFainted)
                break;

            var hitIndex = i;
            var useMove = executingMove.UseMove;
            var hitType = useMove.MoveType;
            executingMove.RunScriptHook(x => x.ChangeMoveType(executingMove, target, hitIndex, ref hitType));

            var hitData = (HitData)executingMove.GetDataFromRawIndex(targetHitStat + i);
            hitData.Type = hitType;

            var effectiveness = battle.Library.StaticLibrary.Types.GetEffectiveness(hitType, target.Types);
            executingMove.RunScriptHook(x => x.ChangeEffectiveness(executingMove, target, hitIndex, ref effectiveness));
            hitData.Effectiveness = effectiveness;
            
            var blockCritical = false;
            executingMove.RunScriptHook(x => x.BlockCriticalHit(executingMove, target, hitIndex, ref blockCritical));
            target.RunScriptHook(x => x.BlockIncomingCriticalHit(executingMove, target, hitIndex, ref blockCritical));
            if (!blockCritical)
            {
                var critical = battle.Library.DamageCalculator.IsCritical(battle, executingMove, target, hitIndex);
                hitData.IsCritical = critical;
            }
            
            var basePower = battle.Library.DamageCalculator.GetBasePower(executingMove, target, hitIndex, hitData);
            hitData.BasePower = basePower;
            
            hitData.Damage = battle.Library.DamageCalculator.GetDamage(executingMove, target, hitIndex, hitData);

            var accuracy = useMove.Accuracy;
            // If the accuracy is 255, the move should always hit, and as such we should not allow
            // modifying it.
            if (accuracy != 255)
            {
                executingMove.RunScriptHook(x => x.ChangeAccuracy(executingMove, target, hitIndex, ref accuracy));
            }
            
            // TODO: Deal with accuracy/evasion stats.
            if (accuracy < 100 && battle.Random.GetInt(100) >= accuracy)
            {
                executingMove.RunScriptHook(x => x.OnMoveMiss(executingMove, target));
                battle.EventHook.Invoke(new MoveMissEvent(executingMove));
                break;
            }

            if (useMove.Category == MoveCategory.Status)
            {
                var secondaryEffect = useMove.SecondaryEffect;
                if (secondaryEffect != null)
                {
                    var chance = secondaryEffect.Chance;
                    if (chance < 0 || battle.Random.EffectChance(chance, executingMove, target, hitIndex))
                    {
                        executingMove.RunScriptHook(x => x.OnSecondaryEffect(executingMove, target, hitIndex));
                    }
                }
            }
            // else if the move is a physical or special move, we should apply the damage.
            else
            {
                var currentHealth = target.CurrentHealth;
                if (hitData.Damage > currentHealth)
                {
                    hitData.Damage = currentHealth;
                }
                var damage = hitData.Damage;
                if (damage > 0)
                {
                    var hitEventBatch = new EventBatchId();
                    battle.EventHook.Invoke(new MoveHitEvent(executingMove, hitData, target)
                    {
                        BatchId = hitEventBatch,
                    });
                    target.Damage(damage, DamageSource.MoveDamage, hitEventBatch);
                    if (!target.IsFainted) 
                        target.RunScriptHook(x => x.OnIncomingHit(executingMove, target, hitIndex));
                    else
                        executingMove.RunScriptHook(x => x.OnOpponentFaints(executingMove, target, hitIndex));

                    if (!target.IsFainted)
                    {
                        var secondaryEffect = useMove.SecondaryEffect;
                        if (secondaryEffect != null)
                        {
                            var preventSecondary = false;
                            target.RunScriptHook(x => x.PreventSecondaryEffect(executingMove, target, hitIndex, ref preventSecondary));

                            if (!preventSecondary)
                            {
                                var chance = secondaryEffect.Chance;
                                if (chance < 0 || battle.Random.EffectChance(chance, executingMove, target, hitIndex))
                                {
                                    executingMove.RunScriptHook(x => x.OnSecondaryEffect(executingMove, target, hitIndex));
                                }
                            }
                        }
                    }
                }
            }
        }
        
        if (numberOfHits == 0)
        {
            target.RunScriptHook(x => x.OnMoveMiss(executingMove, target));
            battle.EventHook.Invoke(new MoveMissEvent(executingMove));
        }

        if (!executingMove.User.IsFainted)
        {
            executingMove.RunScriptHook(x => x.OnAfterHits(executingMove, target));
        }

    }
}