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

namespace PkmnLib.Dynamic.BattleFlow;

/// <summary>
/// Helper class for handling the running of a turn in a battle.
/// </summary>
public static class TurnRunner
{
    /// <summary>
    /// Runs a single turn in a battle to completion.
    /// </summary>
    public static void RunTurn(this IBattle battle)
    {
        var queue = battle.ChoiceQueue;
        if (queue == null)
        {
            throw new ArgumentNullException(nameof(battle.ChoiceQueue),
                "The battle's choice queue must be set before running a turn.");
        }

        // We are now at the very beginning of a turn. We have assigned speeds and priorities to all
        // choices, and put them in the correct order.

        // The first thing to do is to run the on_before_turn script hook on every choice. This script hook
        // is primarily intended to be used to reset variables on a script (for example scripts that need
        // to check whether a Pokémon was hit this turn. By resetting here, and setting a variable to true
        // they can then know this later on.)
        foreach (var choice in queue.GetChoices().WhereNotNull())
        {
            choice.RunScriptHook<IScriptOnBeforeTurnStart>(script => script.OnBeforeTurnStart(choice));
        }

        // Now we can properly begin executing choices.
        // One by one dequeue the turns, and run them. If the battle has ended we do not want to
        // continue running.
        while (queue.HasNext() && !battle.HasEnded)
        {
            var next = queue.Dequeue();
            if (next == null)
                continue;
            ExecuteChoice(battle, next);
        }

        // If the battle is not ended, we have arrived at the normal end of a turn. and thus want
        // to run the end turn scripts.

        // As we want all scripts to run exactly once, including those on the sides and battles,
        // we can't just use the default script hook on each Pokémon. Instead, manually call
        // the script functions on every script.
        if (!battle.HasEnded)
        {
            var scripts = new List<IEnumerable<ScriptContainer>>(10);
            foreach (var side in battle.Sides)
            {
                foreach (var pokemon in side.Pokemon.WhereNotNull())
                {
                    scripts.Clear();
                    pokemon.GetOwnScripts(scripts);
                    scripts.RunScriptHook<IScriptOnEndTurn>(x => x.OnEndTurn(pokemon, battle));
                }
                scripts.Clear();
                side.GetOwnScripts(scripts);
                scripts.RunScriptHook<IScriptOnEndTurn>(x => x.OnEndTurn(side, battle));
            }
            scripts.Clear();
            battle.GetOwnScripts(scripts);
            scripts.RunScriptHook<IScriptOnEndTurn>(x => x.OnEndTurn(battle, battle));
        }
    }

    /// <summary>
    /// Runs a single choice in a battle.
    /// </summary>
    public static void ExecuteChoice(IBattle battle, ITurnChoice choice)
    {
        choice.RunScriptHook<IScriptChangeTurnChoice>(x => x.ChangeTurnChoice(ref choice));
        if (choice is IPassChoice)
            return;
        if (battle.HasEnded)
            return;
        if (!choice.User.IsUsable)
            return;
        if (choice.User.BattleData?.IsOnBattlefield != true)
            return;
        switch (choice)
        {
            case IMoveChoice moveChoice:
                MoveTurnExecutor.ExecuteMoveChoice(battle, moveChoice);
                moveChoice.RunScriptHook<IScriptOnAfterMoveChoice>(script => script.OnAfterMoveChoice(moveChoice));
                break;
            case ISwitchChoice switchChoice:
                ExecuteSwitchChoice(battle, switchChoice);
                break;
            case IFleeChoice fleeChoice:
                ExecuteFleeChoice(battle, fleeChoice);
                break;
            case IItemChoice itemChoice:
                ExecuteItemChoice(battle, itemChoice);
                break;
        }
    }

    private static void ExecuteSwitchChoice(IBattle battle, ISwitchChoice fleeChoice)
    {
        var user = fleeChoice.User;
        var battleData = user.BattleData;
        if (battleData == null)
            return;
        var preventSwitch = false;
        fleeChoice.RunScriptHook<IScriptPreventSelfSwitch>(script =>
            script.PreventSelfSwitch(fleeChoice, ref preventSwitch));
        if (preventSwitch)
            return;
        foreach (var side in battle.Sides)
        {
            if (side.Index == battleData.SideIndex)
                continue;
            foreach (var pokemon in side.Pokemon.WhereNotNull())
            {
                pokemon.RunScriptHook<IScriptPreventOpponentSwitch>(script =>
                    script.PreventOpponentSwitch(fleeChoice, ref preventSwitch));
                if (preventSwitch)
                    return;
            }
        }
        user.Volatile.Clear();
        var userSide = battle.Sides[battleData.SideIndex];
        userSide.SwapPokemon(battleData.Position, fleeChoice.SwitchTo);
    }

    private static void ExecuteFleeChoice(IBattle battle, IFleeChoice fleeChoice)
    {
        var user = fleeChoice.User;
        var battleData = user.BattleData;
        if (battleData == null)
            return;
        if (!battle.CanFlee)
            return;

        var preventFlee = false;
        fleeChoice.RunScriptHook<IScriptPreventSelfRunAway>(script =>
            script.PreventSelfRunAway(fleeChoice, ref preventFlee));
        if (preventFlee)
            return;

        foreach (var side in battle.Sides)
        {
            if (side.Index == battleData.SideIndex)
                continue;
            foreach (var pokemon in side.Pokemon.WhereNotNull())
            {
                pokemon.RunScriptHook<IScriptPreventOpponentRunAway>(script =>
                    script.PreventOpponentRunAway(fleeChoice, ref preventFlee));
                if (preventFlee)
                    return;
            }
        }

        if (!battle.Library.MiscLibrary.CanFlee(battle, fleeChoice))
        {
            battle.EventHook.Invoke(new FleeEvent(user, false));
            return;
        }

        var userSide = battle.Sides[battleData.SideIndex];
        userSide.MarkAsFled();
        battle.EventHook.Invoke(new FleeEvent(user, true));
        battle.ValidateBattleState();
    }

    private static void ExecuteItemChoice(IBattle battle, IItemChoice itemChoice)
    {
        var user = itemChoice.User;
        var battleData = user.BattleData;
        if (battleData == null)
            return;
        var target = itemChoice.GetTargetPokemon(battle);
        battle.EventHook.Invoke(new ItemUseEvent(user, itemChoice.Item));
        itemChoice.Item.RunItemScript(battle.Library.ScriptResolver, target ?? user, user, battle, battle.EventHook);
    }
}