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

namespace PkmnLib.Dynamic.Models;

/// <summary>
/// A side in a battle.
/// </summary>
public interface IBattleSide : IScriptSource, IDeepCloneable
{
    /// <summary>
    /// The index of the side on the battle.
    /// </summary>
    byte Index { get; }

    /// <summary>
    /// The number of Pokémon that can be on the side.
    /// </summary>
    byte NumberOfPositions { get; }

    /// <summary>
    /// A list of Pokémon currently on the battlefield.
    /// </summary>
    IReadOnlyList<IPokemon?> Pokemon { get; }

    /// <summary>
    /// The currently set choices for all Pokémon on the battlefield. Cleared when the turn starts.
    /// </summary>
    IReadOnlyList<ITurnChoice?> SetChoices { get; }

    /// <summary>
    /// Whether every Pokémon on this side has its choices  
    /// </summary>
    bool AllChoicesSet { get; }

    /// <summary>
    /// The slots on the side that can still be filled. Once all slots are set to false, this side
    /// has lost the battle.
    /// </summary>
    IReadOnlyList<bool> FillablePositions { get; }

    /// <summary>
    /// A reference to the battle this side is in.
    /// </summary>
    IBattle Battle { get; }

    /// <summary>
    /// Whether this side has fled.
    /// </summary>
    bool HasFledBattle { get; }

    /// <summary>
    /// The volatile scripts that are attached to the side.
    /// </summary>
    IScriptSet VolatileScripts { get; }

    /// <summary>
    /// Returns true if there are slots that need to be filled with a new pokemon, that have parties
    /// responsible for them. Returns false if all slots are filled with usable pokemon, or slots are
    /// empty, but can't be filled by any party anymore.
    /// </summary>
    bool AllPositionsFilled();

    /// <summary>
    /// Sets a choice for a Pokémon on this side.
    /// </summary>
    void SetChoice(byte position, ITurnChoice choice);

    /// <summary>
    /// Resets all choices on this side.
    /// </summary>
    void ResetChoices();

    /// <summary>
    /// Forcibly removes a Pokémon from the field.
    /// </summary>
    /// <param name="index"></param>
    void ForceClearPokemonFromField(byte index);

    /// <summary>
    /// Switches out a spot on the field for a different Pokémon. If null is passed, the spot is
    /// cleared. Returns the Pokémon that was previously in the spot.
    /// </summary>
    IPokemon? SwapPokemon(byte position, IPokemon? pokemon);

    /// <summary>
    /// Swaps two Pokémon on the side.
    /// </summary>
    void SwapPokemon(byte position1, byte position2);

    /// <summary>
    /// Checks whether a Pokemon is on the field in this side.
    /// </summary>
    bool IsPokemonOnSide(IPokemon pokemon);

    /// <summary>
    /// Marks a slot as unfillable. This happens when no parties are able to fill the slot anymore.
    /// If this happens, the slot can not be used again.
    /// </summary>
    void MarkPositionAsUnfillable(byte position);

    /// <summary>
    /// Checks whether a slot is fillable. If it is not, the slot can not be used anymore.
    /// </summary>
    bool IsPositionFillable(byte position);

    /// <summary>
    /// Checks whether the side has been defeated.
    /// </summary>
    bool IsDefeated();

    /// <summary>
    /// The number of times this side has attempted to flee.
    /// </summary>
    uint FleeAttempts { get; }

    /// <summary>
    /// Registers a flee attempt for this side.
    /// </summary>
    void RegisterFleeAttempt();

    /// <summary>
    /// Mark the side as fled.
    /// </summary>
    void MarkAsFled();

    /// <summary>
    /// Gets a random Pokémon on the given side.
    /// </summary>
    byte GetRandomPosition();

    /// <summary>
    /// Marks an item as consumed for a position. Can be used by moves such as Recycle to get the item back.
    /// </summary>
    void SetConsumedItem(byte battleDataPosition, IItem heldItem);

    /// <summary>
    /// Gets the last consumed item for a position. Can be used by moves such as Recycle to get the item back.
    /// </summary>
    IItem? GetLastConsumedItem(byte battleDataPosition);

    /// <summary>
    /// Marks a Pokémon as fainted. This is used to track the last turn a Pokémon in a position fainted.
    /// </summary>
    void MarkFaint(byte position);

    /// <summary>
    /// Gets the last turn a Pokémon in a specific position fainted.
    /// </summary>
    uint? GetLastFaintTurn(byte position);

    /// <summary>
    /// Gets the last turn a Pokémon in any position fainted.
    /// </summary>
    uint? GetLastFaintTurn();
}

/// <inheritdoc cref="IBattleSide"/>
public class BattleSideImpl : ScriptSource, IBattleSide
{
    /// <inheritdoc cref="BattleSideImpl"/>
    public BattleSideImpl(byte index, byte numberOfPositions, IBattle battle)
    {
        Index = index;
        NumberOfPositions = numberOfPositions;
        _pokemon = new IPokemon?[numberOfPositions];
        _setChoices = new ITurnChoice?[numberOfPositions];
        _fillablePositions = new bool[numberOfPositions];
        for (byte i = 0; i < numberOfPositions; i++)
        {
            _fillablePositions[i] = true;
        }
        Battle = battle;
        VolatileScripts = new ScriptSet(this);
    }

    /// <inheritdoc />
    public byte Index { get; }

    /// <inheritdoc />
    public byte NumberOfPositions { get; }

    private readonly IPokemon?[] _pokemon;

    /// <inheritdoc />
    public IReadOnlyList<IPokemon?> Pokemon => _pokemon;

    private readonly ITurnChoice?[] _setChoices;

    /// <inheritdoc />
    public IReadOnlyList<ITurnChoice?> SetChoices => _setChoices;

    /// <inheritdoc />
    public bool AllChoicesSet => _setChoices.All(choice => choice is not null);

    private readonly bool[] _fillablePositions;

    /// <inheritdoc />
    public IReadOnlyList<bool> FillablePositions => _fillablePositions;

    /// <inheritdoc />
    public IBattle Battle { get; }

    /// <inheritdoc />
    public bool HasFledBattle { get; private set; }

    /// <inheritdoc />
    public IScriptSet VolatileScripts { get; }

    /// <inheritdoc />
    public bool AllPositionsFilled()
    {
        for (byte i = 0; i < NumberOfPositions; i++)
        {
            var pokemon = Pokemon[i];
            var isPokemonViable = pokemon is not null && pokemon.IsUsable;
            // If the Pokémon is not valid, but the slot can be filled, return false.
            if (!isPokemonViable && Battle.CanSlotBeFilled(Index, i))
            {
                return false;
            }
        }
        return true;
    }

    /// <inheritdoc />
    public void SetChoice(byte position, ITurnChoice choice)
    {
        _setChoices[position] = choice;
    }

    /// <inheritdoc />
    public void ResetChoices()
    {
        for (byte i = 0; i < NumberOfPositions; i++)
        {
            _setChoices[i] = null;
        }
    }

    /// <param name="index"></param>
    /// <inheritdoc />
    public void ForceClearPokemonFromField(byte index)
    {
        var pokemon = _pokemon[index];
        if (pokemon is not null)
        {
            pokemon.RunScriptHook<IScriptOnRemove>(script => script.OnRemove());
            pokemon.SetOnBattlefield(false);
        }

        _pokemon[index] = null;
    }

    /// <inheritdoc />
    public IPokemon? SwapPokemon(byte position, IPokemon? pokemon)
    {
        var oldPokemon = _pokemon[position];
        if (oldPokemon is not null)
        {
            oldPokemon.RunScriptHook<IScriptOnSwitchOut>(script => script.OnSwitchOut(oldPokemon, position));
            oldPokemon.RunScriptHook<IScriptOnRemove>(script => script.OnRemove());
            oldPokemon.SetOnBattlefield(false);
        }
        _pokemon[position] = pokemon;
        if (pokemon is not null)
        {
            pokemon.SetBattleData(Battle, Index);
            pokemon.SetOnBattlefield(true);
            pokemon.SetBattleSidePosition(position);
            Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon));
            pokemon.RunScriptHook<IScriptOnSwitchIn>(script => script.OnSwitchIn(pokemon, position));

            foreach (var side in Battle.Sides)
            {
                if (side == this)
                    continue;
                var scripts = new List<IEnumerable<ScriptContainer>>(10);
                foreach (var opponent in side.Pokemon.WhereNotNull())
                {
                    opponent.MarkOpponentAsSeen(pokemon);
                    pokemon.MarkOpponentAsSeen(opponent);

                    scripts.Clear();
                    opponent.GetOwnScripts(scripts);
                    opponent.RunScriptHook<IScriptOnOpponentSwitchIn>(script =>
                        script.OnOpponentSwitchIn(pokemon, position));
                }
                side.RunScriptHook<IScriptOnOpponentSwitchIn>(script => script.OnOpponentSwitchIn(pokemon, position));
            }
        }
        else
        {
            Battle.EventHook.Invoke(new SwitchEvent(Index, position, null));
        }

        return oldPokemon;
    }

    /// <inheritdoc />
    public void SwapPokemon(byte position1, byte position2)
    {
        throw new NotImplementedException();
    }

    /// <inheritdoc />
    public bool IsPokemonOnSide(IPokemon pokemon) => _pokemon.Contains(pokemon);

    /// <inheritdoc />
    public void MarkPositionAsUnfillable(byte position) => _fillablePositions[position] = false;

    /// <inheritdoc />
    public bool IsPositionFillable(byte position) => _fillablePositions[position];

    /// <inheritdoc />
    public bool IsDefeated()
    {
        return _fillablePositions.All(fillable => !fillable);
    }

    /// <inheritdoc />
    public uint FleeAttempts { get; private set; }

    /// <inheritdoc />
    public void RegisterFleeAttempt()
    {
        FleeAttempts++;
    }

    /// <inheritdoc />
    public void MarkAsFled() => HasFledBattle = true;

    /// <inheritdoc />
    public byte GetRandomPosition() => (byte)Battle.Random.GetInt(0, NumberOfPositions);

    private Dictionary<byte, IItem>? _lastConsumedItems;

    /// <inheritdoc />
    public void SetConsumedItem(byte battleDataPosition, IItem heldItem)
    {
        _lastConsumedItems ??= new Dictionary<byte, IItem>();
        _lastConsumedItems[battleDataPosition] = heldItem;
    }

    /// <inheritdoc />
    public IItem? GetLastConsumedItem(byte battleDataPosition) =>
        _lastConsumedItems?.GetValueOrDefault(battleDataPosition);

    private Dictionary<byte, uint>? _lastFaintTurn;

    /// <inheritdoc />
    public void MarkFaint(byte position)
    {
        _lastFaintTurn ??= new Dictionary<byte, uint>();
        _lastFaintTurn[position] = Battle.CurrentTurnNumber;
    }

    /// <inheritdoc />
    public uint? GetLastFaintTurn(byte position)
    {
        if (_lastFaintTurn is null)
            return null;
        if (_lastFaintTurn.TryGetValue(position, out var turn))
            return turn;
        return null;
    }

    /// <inheritdoc />
    public uint? GetLastFaintTurn() =>
        _lastFaintTurn?.Values.Max() ?? null;

    /// <inheritdoc />
    public override int ScriptCount => 1 + Battle.ScriptCount;

    /// <inheritdoc />
    public override void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts)
    {
        scripts.Add(VolatileScripts);
    }

    /// <inheritdoc />
    public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts)
    {
        scripts.Add(VolatileScripts);
        Battle.CollectScripts(scripts);
    }
}