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

namespace PkmnLib.Dynamic.BattleFlow;

/// <summary>
/// Helper class for executing moves.
/// </summary>
public static class MoveTurnExecutor
{
    internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
    {
        moveChoice.User.BattleData!.LastMoveChoice = moveChoice;
        var chosenMove = moveChoice.ChosenMove;
        var useMove = chosenMove.MoveData;

        var moveDataName = useMove.Name;
        moveChoice.RunScriptHook<IScriptChangeMove>(x => x.ChangeMove(moveChoice, ref moveDataName));
        if (useMove.Name != moveDataName)
        {
            if (!battle.Library.StaticLibrary.Moves.TryGet(moveDataName, out useMove))
            {
                throw new InvalidOperationException(
                    $"The move was changed to '{moveDataName}' by a script, but this move does not exist.");
            }
            var secondaryEffect = useMove.SecondaryEffect;
            if (secondaryEffect != null)
            {
                if (moveChoice.User.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name,
                        secondaryEffect.Parameters, out var script))
                {
                    moveChoice.Script.Set(script);
                    script.OnAddedToParent(moveChoice);
                }
                else
                {
                    moveChoice.Script.Clear();
                }
            }
            else
            {
                moveChoice.Script.Clear();
            }
        }
        moveChoice.RunScriptHook<IScriptOnBeforeMoveChoice>(x => x.OnBeforeMoveChoice(moveChoice));

        var targetType = useMove.Target;
        var targets =
            TargetResolver.ResolveTargets(battle, moveChoice.TargetSide, moveChoice.TargetPosition, targetType);
        moveChoice.RunScriptHook<IScriptChangeTargets>(x => x.ChangeTargets(moveChoice, ref targets));
        if (targets.Count == 0)
        {
            moveChoice.Fail();
            return;
        }

        foreach (var target in targets.WhereNotNull())
        {
            target.RunScriptHook<IScriptChangeIncomingTargets>(x => x.ChangeIncomingTargets(moveChoice, ref targets));
        }

        byte numberOfHits = 1;
        moveChoice.RunScriptHook<IScriptChangeNumberOfHits>(x => x.ChangeNumberOfHits(moveChoice, ref numberOfHits));
        if (numberOfHits == 0)
        {
            return;
        }

        var executingMove = new ExecutingMoveImpl(targets, numberOfHits, chosenMove, useMove, moveChoice, battle);

        battle.EventHook.Invoke(new MoveUseEvent(executingMove));

        var prevented = false;
        executingMove.RunScriptHook<IScriptPreventMove>(x => x.PreventMove(executingMove, ref prevented));
        if (prevented)
            return;

        byte ppUsed = 1;
        executingMove.RunScriptHook<IScriptModifyPPUsed>(x => x.ModifyPPUsed(executingMove, ref ppUsed));
        targets.WhereNotNull()
            .RunScriptHook<IScriptModifyPPUsedForIncomingMove>(x =>
                x.ModifyPPUsedForIncomingMove(executingMove, ref ppUsed));
        if (!executingMove.ChosenMove.TryUse(ppUsed))
            return;

        var failed = false;
        executingMove.RunScriptHook<IScriptFailMove>(x => x.FailMove(executingMove, ref failed));
        if (failed)
        {
            // TODO: fail handling
            executingMove.MoveChoice.Fail();
            return;
        }
        ExecuteMove(executingMove);

        if (executingMove.Hits.All(x => x.HasFailed) || (executingMove.UseMove.Category != MoveCategory.Status &&
                                                         executingMove.Hits.All(x => x.Damage == 0)))
        {
            executingMove.MoveChoice.Fail();
        }
    }

    /// <summary>
    /// Executes the move for its targets.
    /// </summary>
    /// <param name="executingMove"></param>
    public static void ExecuteMove(IExecutingMove executingMove)
    {
        var stopped = false;
        executingMove.RunScriptHook<IScriptStopBeforeMove>(x => x.StopBeforeMove(executingMove, ref stopped));
        if (stopped)
            return;

        executingMove.RunScriptHook<IScriptOnBeforeMove>(x => x.OnBeforeMove(executingMove));
        foreach (var target in executingMove.Targets.WhereNotNull())
        {
            ExecuteMoveChoiceForTarget(executingMove.Battle, executingMove, target);
        }
        executingMove.RunScriptHook<IScriptOnAfterMove>(x => x.OnAfterMove(executingMove));
    }

    private static readonly ThreadLocal<List<TypeIdentifier>> TypeListCache = new(() => []);

    private static void ExecuteMoveChoiceForTarget(IBattle battle, IExecutingMove executingMove, IPokemon target)
    {
        var failed = false;
        target.RunScriptHook<IScriptFailIncomingMove>(x => x.FailIncomingMove(executingMove, target, ref failed));
        if (failed)
        {
            // TODO: fail handling
            return;
        }

        var isInvulnerable = false;
        target.RunScriptHook<IScriptIsInvulnerableToMove>(x =>
            x.IsInvulnerableToMove(executingMove, target, ref isInvulnerable));
        if (isInvulnerable)
        {
            battle.EventHook.Invoke(new MoveInvulnerableEvent(executingMove, target));
            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;
            executingMove.RunScriptHook<IScriptOnBeforeHit>(x => x.OnBeforeHit(executingMove, target, hitIndex));
            var hitData = (HitData)executingMove.GetDataFromRawIndex(targetHitStat + i);
            if (hitData.HasFailed)
                break;

            var useMove = executingMove.UseMove;

            var isContact = useMove.HasFlag("contact");
            executingMove.RunScriptHook<IScriptModifyIsContact>(x =>
                x.ModifyIsContact(executingMove, target, hitIndex, ref isContact));
            hitData.IsContact = isContact;

            var hitType = (TypeIdentifier?)useMove.MoveType;
            executingMove.RunScriptHook<IScriptChangeMoveType>(x =>
                x.ChangeMoveType(executingMove, target, hitIndex, ref hitType));

            hitData.Type = hitType;

            // We re-use the TypeListCache to avoid allocating a new list every time.
            var types = TypeListCache.Value;
            types.Clear();
            types.AddRange(target.Types);

            executingMove.RunScriptHook<IScriptChangeTypesForMove>(x =>
                x.ChangeTypesForMove(executingMove, target, hitIndex, types));
            target.RunScriptHook<IScriptChangeTypesForIncomingMove>(x =>
                x.ChangeTypesForIncomingMove(executingMove, target, hitIndex, types));

            var effectiveness = hitType == null
                ? 1
                : battle.Library.StaticLibrary.Types.GetEffectiveness(hitType.Value, types);
            executingMove.RunScriptHook<IScriptChangeEffectiveness>(x =>
                x.ChangeEffectiveness(executingMove, target, hitIndex, ref effectiveness));
            target.RunScriptHook<IScriptChangeIncomingEffectiveness>(x =>
                x.ChangeIncomingEffectiveness(executingMove, target, hitIndex, ref effectiveness));
            hitData.Effectiveness = effectiveness;

            var blockCritical = false;
            executingMove.RunScriptHook<IScriptBlockCriticalHit>(x =>
                x.BlockCriticalHit(executingMove, target, hitIndex, ref blockCritical));
            target.RunScriptHook<IScriptBlockIncomingCriticalHit>(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, executingMove.UseMove.Category,
                executingMove.User, target, executingMove.TargetCount, 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)
            {
                accuracy = battle.Library.StatCalculator.CalculateModifiedAccuracy(executingMove, target, hitIndex,
                    accuracy);
            }

            if (accuracy < 100 && battle.Random.GetInt(100) >= accuracy)
            {
                executingMove.RunScriptHook<IScriptOnMoveMiss>(x => x.OnMoveMiss(executingMove, target));
                battle.EventHook.Invoke(new MoveMissEvent(executingMove));
                break;
            }

            var blockIncomingHit = false;
            target.RunScriptHook<IScriptBlockIncomingHit>(x =>
                x.BlockIncomingHit(executingMove, target, hitIndex, ref blockIncomingHit));
            executingMove.RunScriptHook<IScriptBlockOutgoingHit>(x =>
                x.BlockOutgoingHit(executingMove, target, hitIndex, ref blockIncomingHit));
            if (blockIncomingHit)
                break;
            if (executingMove.GetHitData(target, hitIndex).HasFailed)
                break;
            var category = useMove.Category;
            executingMove.RunScriptHook<IScriptChangeCategory>(x =>
                x.ChangeCategory(executingMove, target, hitIndex, ref category));
            if (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<IScriptOnSecondaryEffect>(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<IScriptOnIncomingHit>(x =>
                            x.OnIncomingHit(executingMove, target, hitIndex));
                    }
                    else
                    {
                        executingMove.RunScriptHook<IScriptOnOpponentFaints>(x =>
                            x.OnOpponentFaints(executingMove, target, hitIndex));
                    }

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

                            if (!preventSecondary)
                            {
                                var chance = secondaryEffect.Chance;
                                if (chance < 0 || battle.Random.EffectChance(chance, executingMove, target, hitIndex))
                                {
                                    executingMove.RunScriptHook<IScriptOnSecondaryEffect>(x =>
                                        x.OnSecondaryEffect(executingMove, target, hitIndex));
                                }
                            }
                        }
                        if (target.IsFainted)
                        {
                            executingMove.RunScriptHook<IScriptOnOpponentFaints>(x =>
                                x.OnOpponentFaints(executingMove, target, hitIndex));
                        }
                    }
                }
            }
        }

        if (numberOfHits == 0)
        {
            target.RunScriptHook<IScriptOnMoveMiss>(x => x.OnMoveMiss(executingMove, target));
            battle.EventHook.Invoke(new MoveMissEvent(executingMove));
        }

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