using System.Diagnostics.CodeAnalysis;
using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.Models.Serialized;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;
using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dynamic.Models;

/// <summary>
/// The data of a Pokemon.
/// </summary>
public interface IPokemon : IScriptSource, IDeepCloneable
{
    /// <summary>
    /// The library data of the Pokemon.
    /// </summary>
    IDynamicLibrary Library { get; }

    /// <summary>
    /// The species of the Pokemon.
    /// </summary>
    ISpecies Species { get; }

    /// <summary>
    /// The form of the Pokemon.
    /// </summary>
    IForm Form { get; }

    /// <summary>
    /// An optional display species of the Pokemon. If this is set, the client should display this
    /// species. An example of usage for this is the Illusion ability.
    /// </summary>
    ISpecies? DisplaySpecies { get; }

    /// <summary>
    /// An optional display form of the Pokemon. If this is set, the client should display this
    /// form. An example of usage for this is the Illusion ability.
    /// </summary>
    IForm? DisplayForm { get; }

    /// <summary>
    /// Sets the display species and form of the Pokemon. This is used for abilities like Illusion.
    /// </summary>
    void SetDisplaySpecies(ISpecies? species, IForm? form);

    /// <summary>
    /// The current level of the Pokemon.
    /// </summary>
    LevelInt Level { get; }

    /// <summary>
    /// The amount of experience of the Pokemon.
    /// </summary>
    uint Experience { get; }

    /// <summary>
    /// Increases the experience of the Pokemon. Returns whether any experience was gained.
    /// </summary>
    bool AddExperience(uint experience);

    /// <summary>
    /// The personality value of the Pokemon.
    /// </summary>
    uint PersonalityValue { get; }

    /// <summary>
    /// The gender of the Pokemon.
    /// </summary>
    Gender Gender { get; }

    /// <summary>
    /// The coloring of the Pokemon. Value 0 is the default, value 1 means shiny. Other values are
    /// currently not used, and can be used for other implementations.
    /// </summary>
    byte Coloring { get; }

    /// <summary>
    /// Whether the Pokemon is shiny.
    /// </summary>
    bool IsShiny { get; }

    /// <summary>
    /// The held item of the Pokemon.
    /// </summary>
    IItem? HeldItem { get; }

    /// <summary>
    /// The remaining health points of the Pokemon.
    /// </summary>
    uint CurrentHealth { get; }

    /// <summary>
    /// The weight of the Pokemon in kilograms.
    /// </summary>
    float WeightInKg { get; }

    /// <summary>
    /// The height of the Pokémon in meters.
    /// </summary>
    float HeightInMeters { get; set; }

    /// <summary>
    /// The happiness of the Pokemon. Also known as friendship.
    /// </summary>
    byte Happiness { get; set; }

    /// <summary>
    /// The stats of the Pokemon when disregarding any stat boosts.
    /// </summary>
    StatisticSet<uint> FlatStats { get; }

    /// <summary>
    /// The statistics boosts of the Pokemon. Will prevent the value from going above 6, and below
    /// -6.
    /// </summary>
    StatBoostStatisticSet StatBoost { get; }

    /// <summary>
    /// The stats of the Pokemon including the stat boosts
    /// </summary>
    StatisticSet<uint> BoostedStats { get; }

    /// <summary>
    /// The maximum health of the Pokemon.
    /// </summary>
    uint MaxHealth { get; }

    /// <summary>
    /// The individual values of the Pokemon.
    /// </summary>
    IndividualValueStatisticSet IndividualValues { get; }

    /// <summary>
    /// The effort values of the Pokemon.
    /// </summary>
    EffortValueStatisticSet EffortValues { get; }

    /// <summary>
    /// The nature of the Pokemon.
    /// </summary>
    INature Nature { get; }

    /// <summary>
    /// An optional nickname of the Pokemon.
    /// </summary>
    string? Nickname { get; }

    /// <summary>
    /// An index of the ability to find the actual ability on the form.
    /// </summary>
    AbilityIndex AbilityIndex { get; }

    /// <summary>
    /// An ability can be overriden to an arbitrary ability. This is for example used for the Mummy
    /// ability.
    /// </summary>
    IAbility? OverrideAbility { get; }

    /// <summary>
    /// If in battle, we have additional data.
    /// </summary>
    IPokemonBattleData? BattleData { get; }

    /// <summary>
    /// The moves the Pokemon has learned. This is of a set length of <see cref="Const.MovesCount"/>. Empty move slots
    /// are null.
    /// </summary>
    IReadOnlyList<ILearnedMove?> Moves { get; }

    /// <summary>
    /// Checks whether the Pokemon has a specific move in its current moveset.
    /// </summary>
    bool HasMove(StringKey moveName);

    /// <summary>
    /// Swaps two moves of the Pokemon.
    /// </summary>
    void SwapMoves(byte index1, byte index2);

    /// <summary>
    /// Whether or not the Pokemon is allowed to gain experience.
    /// </summary>
    bool AllowedExperience { get; }

    /// <summary>
    /// The current types of the Pokemon.
    /// </summary>
    IReadOnlyList<TypeIdentifier> Types { get; }

    /// <summary>
    /// Whether or not this Pokemon is an egg.
    /// </summary>
    bool IsEgg { get; }

    /// <summary>
    /// Whether or not this Pokemon was caught this battle.
    /// </summary>
    bool IsCaught { get; }

    /// <summary>
    /// Marks the Pokemon as caught. This makes it so that the Pokemon is not considered valid in battle anymore.
    /// </summary>
    void MarkAsCaught();

    /// <summary>
    /// The script for the held item.
    /// </summary>
    ScriptContainer HeldItemTriggerScript { get; }

    /// <summary>
    /// The script for the ability.
    /// </summary>
    ScriptContainer AbilityScript { get; }

    /// <summary>
    /// The script for the status.
    /// </summary>
    ScriptContainer StatusScript { get; }

    /// <summary>
    /// The number of turns left for the current non-volatile status.
    /// </summary>
    int? GetStatusTurnsLeft { get; }

    /// <summary>
    /// The volatile status scripts of the Pokemon.
    /// </summary>
    IScriptSet Volatile { get; }

    /// <summary>
    /// Checks whether the Pokemon is holding an item with a specific name.
    /// </summary>
    bool HasHeldItem(StringKey itemName);

    /// <summary>
    /// Changes the held item of the Pokemon. Returns the previously held item.
    /// </summary>
    [MustUseReturnValue]
    IItem? ForceSetHeldItem(IItem? item);

    /// <summary>
    /// Removes the held item from the Pokemon. Returns the previously held item.
    /// </summary>
    [MustUseReturnValue]
    IItem? RemoveHeldItem();

    /// <summary>
    /// Whether the held item has been removed for the duration of the battle.
    /// </summary>
    bool HasItemBeenRemovedForBattle { get; }

    /// <summary>
    /// Removes the held item from the Pokemon for the duration of the battle. Returns the previously held item.
    /// </summary>
    /// <remarks>
    /// This is used for moves that remove a held item, but do not consume it. In this case, the item needs to be
    /// restored after the battle.
    /// </remarks>
    IItem? RemoveHeldItemForBattle();

    /// <summary>
    /// Tries to steal the held item of the Pokémon. If successful, the item is removed from the Pokémon and returned.
    /// If the Pokémon does not have a held item, or the item is a form changer, this will return false.
    /// </summary>
    bool TryStealHeldItem([NotNullWhen(true)] out IItem? item);

    /// <summary>
    /// Restores the held item of a Pokémon if it was temporarily removed.
    /// </summary>
    void RestoreRemovedHeldItem();

    /// <summary>
    /// Makes the Pokemon uses its held item. Returns whether the item was consumed.
    /// </summary>
    bool ConsumeHeldItem();

    /// <summary>
    /// Uses an item on the Pokemon.
    /// </summary>
    void UseItem(IItem item);

    /// <summary>
    /// Change a boosted stat by a certain amount.
    /// </summary>
    /// <param name="stat">The stat to be changed</param>
    /// <param name="change">The amount to change the stat by</param>
    /// <param name="selfInflicted">Whether the change was self-inflicted. This can be relevant in scripts.</param>
    /// <param name="force"></param>
    /// <param name="batchId">The event batch ID this change is a part of. This is relevant for visual handling</param>
    bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, EventBatchId batchId = default);

    /// <summary>
    /// Suppresses the ability of the Pokémon.
    /// </summary>
    bool SuppressAbility();

    /// <summary>
    /// Returns the currently active ability.
    /// </summary>
    IAbility? ActiveAbility { get; }

    /// <summary>
    /// Calculates the flat stats on the Pokemon. This should be called when for example the base
    /// stats, level, nature, IV, or EV changes. This has a side effect of recalculating the boosted
    /// stats, as those depend on the flat stats.
    /// </summary>
    void RecalculateFlatStats();

    /// <summary>
    /// Calculates the boosted stats on the Pokemon, _without_ recalculating the flat stats.
    /// This should be called when a stat boost changes.
    /// </summary>
    void RecalculateBoostedStats();

    /// <summary>
    /// Evolves the Pokemon to a specific evolution. This will not check whether the evolution is valid, so
    /// you should check that before calling this method.
    /// </summary>
    bool EvolveTo(IEvolution evolution, EventHook? eventHook = null);

    /// <summary>
    /// Change the species of the Pokemon.
    /// </summary>
    void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null);

    /// <summary>
    /// Change the form of the Pokemon.
    /// </summary>
    void ChangeForm(IForm form, EventBatchId batchId = default, EventHook? eventHook = null);

    /// <summary>
    /// Whether the Pokemon is useable in a battle.
    /// </summary>
    bool IsUsable { get; }

    /// <summary>
    /// Whether the Pokemon is fainted.
    /// </summary>
    bool IsFainted { get; }

    /// <summary>
    /// Damages the Pokemon by a certain amount of damage, from a damage source.
    /// </summary>
    void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false);

    /// <summary>
    /// Forces the Pokémon to faint.
    /// </summary>
    void Faint(DamageSource source, EventBatchId batchId = default);

    /// <summary>
    /// Heals the Pokemon by a specific amount. Unless allow_revive is set to true, this will not
    /// heal if the Pokemon has 0 health. If the amount healed is 0, this will return false.
    /// </summary>
    bool Heal(uint heal, bool allowRevive = false, EventBatchId batchId = default, bool forceHeal = false,
        EventHook? customEventHook = null);

    /// <summary>
    /// Restores all PP of the Pokemon.
    /// </summary>
    void RestoreAllPP();

    /// <summary>
    /// Learn a move by name.
    /// </summary>
    void LearnMove(StringKey moveName, MoveLearnMethod method, byte index);

    /// <summary>
    /// Checks whether the Pokémon has a specific non-volatile status.
    /// </summary>
    bool HasStatus(StringKey status);

    /// <summary>
    /// Adds a non-volatile status to the Pokemon.
    /// </summary>
    bool SetStatus(StringKey status, IPokemon? originPokemon, EventBatchId batchId = default);

    /// <summary>
    /// Removes the current non-volatile status from the Pokemon.
    /// </summary>
    void ClearStatus(EventBatchId batchId = default);

    /// <summary>
    /// Modifies the level by a certain amount
    /// </summary>
    void ChangeLevelBy(int change);

    /// <summary>
    /// Sets the current battle the Pokémon is in.
    /// </summary>
    void SetBattleData(IBattle battle, byte sideIndex);

    /// <summary>
    /// Sets whether the Pokémon is on the battlefield.
    /// </summary>
    void SetOnBattlefield(bool onBattleField);

    /// <summary>
    /// Sets the position the Pokémon has within its side.
    /// </summary>
    /// <param name="position"></param>
    void SetBattleSidePosition(byte position);

    /// <summary>
    /// Resets the battle data of the Pokémon. This is called when the battle ends.
    /// </summary>
    void ClearBattleData();

    /// <summary>
    /// Marks a Pokemon as seen in the battle.
    /// </summary>
    void MarkOpponentAsSeen(IPokemon pokemon);

    /// <summary>
    /// Removes a type from the Pokémon. Returns whether the type was removed.
    /// </summary>
    bool RemoveType(TypeIdentifier type);

    /// <summary>
    /// Adds a type to the Pokémon. Returns whether the type was added. It will not add the type if
    /// the Pokémon already has it.
    /// </summary>
    bool AddType(TypeIdentifier type);

    /// <summary>
    /// Replace the types of the Pokémon with the provided types.
    /// </summary>
    void SetTypes(IReadOnlyList<TypeIdentifier> types);

    /// <summary>
    /// Changes the ability of the Pokémon.
    /// </summary>
    bool ChangeAbility(IAbility ability);

    /// <summary>
    /// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as
    /// Levitate.
    /// </summary>
    bool IsFloating { get; }

    /// <summary>
    /// Converts the data structure to a serializable format.
    /// </summary>
    SerializedPokemon Serialize();
}

/// <summary>
/// The data of the Pokémon related to being in a battle.
/// This is only set when the Pokémon is on the field in a battle.
/// </summary>
public interface IPokemonBattleData : IDeepCloneable
{
    /// <summary>
    /// Sets the battle data of the Pokémon.
    /// </summary>
    void SetBattle(IBattle battle, byte sideIndex, uint switchInTurn);

    /// <summary>
    /// Sets the position of the Pokémon on the field.
    /// </summary>
    void SetPosition(byte position);

    /// <summary>
    /// The battle the Pokémon is in.
    /// </summary>
    IBattle Battle { get; }

    /// <summary>
    /// The index of the side of the Pokémon
    /// </summary>
    byte SideIndex { get; }

    /// <summary>
    /// The index of the position of the Pokémon on the field
    /// </summary>
    byte Position { get; }

    /// <summary>
    /// A list of opponents the Pokémon has seen this battle.
    /// </summary>
    IReadOnlyList<IPokemon> SeenOpponents { get; }

    /// <summary>
    /// Sets whether the Pokémon is on the battlefield.
    /// </summary>
    void SetOnBattlefield(bool onBattleField);

    /// <summary>
    /// Whether the Pokémon is on the battlefield.
    /// </summary>
    bool IsOnBattlefield { get; }

    /// <summary>
    /// Adds an opponent to the list of seen opponents.
    /// </summary>
    void MarkOpponentAsSeen(IPokemon opponent);

    /// <summary>
    /// A list of items the Pokémon has consumed this battle.
    /// </summary>
    IReadOnlyList<IItem> ConsumedItems { get; }

    /// <summary>
    /// Marks an item as consumed.
    /// </summary>
    void MarkItemAsConsumed(IItem item);

    /// <summary>
    /// The turn the Pokémon switched in.
    /// </summary>
    uint SwitchInTurn { get; }

    /// <summary>
    /// The number of turns the Pokémon has been on the field.
    /// </summary>
    uint TurnsOnField { get; }

    /// <summary>
    /// The side the Pokémon is on.
    /// </summary>
    IBattleSide BattleSide { get; }

    /// <summary>
    /// The species of the Pokémon at the time it was sent out.
    /// </summary>
    ISpecies OriginalSpecies { get; }

    /// <summary>
    /// The form of the Pokémon at the time it was sent out.
    /// </summary>
    IForm OriginalForm { get; }

    /// <summary>
    /// The last move choice executed by the Pokémon.
    /// </summary>
    IMoveChoice? LastMoveChoice { get; set; }
}

/// <inheritdoc cref="IPokemon"/>
public class PokemonImpl : ScriptSource, IPokemon
{
    /// <inheritdoc cref="PokemonImpl"/>
    public PokemonImpl(IDynamicLibrary library, ISpecies species, IForm form, AbilityIndex abilityIndex, LevelInt level,
        uint personalityValue, Gender gender, byte coloring, StringKey natureName)
    {
        Library = library;
        Species = species;
        Form = form;
        AbilityIndex = abilityIndex;
        Level = level;
        PersonalityValue = personalityValue;
        Gender = gender;
        Coloring = coloring;
        Types = form.Types.ToList();

        Experience = library.StaticLibrary.GrowthRates.CalculateExperience(species.GrowthRate, level);
        HeightInMeters = form.Height;
        Happiness = species.BaseHappiness;
        Volatile = new ScriptSet(this);
        if (!library.StaticLibrary.Natures.TryGet(natureName, out var nature))
            throw new KeyNotFoundException($"Nature {natureName} not found.");
        Nature = nature;

        RecalculateFlatStats();
        CurrentHealth = BoostedStats.Hp;
    }

    /// <inheritdoc cref="PokemonImpl"/>
    public PokemonImpl(IDynamicLibrary library, SerializedPokemon serializedPokemon)
    {
        Library = library;
        if (!library.StaticLibrary.Species.TryGet(serializedPokemon.Species, out var species))
            throw new KeyNotFoundException($"Species {serializedPokemon.Species} not found.");
        Species = species;
        if (!species.TryGetForm(serializedPokemon.Form, out var form))
            throw new KeyNotFoundException($"Form {serializedPokemon.Form} not found on species {species.Name}.");
        Form = form;
        Level = serializedPokemon.Level;
        Experience = serializedPokemon.Experience;
        PersonalityValue = serializedPokemon.PersonalityValue;
        Gender = serializedPokemon.Gender;
        Coloring = serializedPokemon.Coloring;
        if (serializedPokemon.HeldItem != null)
        {
            if (!library.StaticLibrary.Items.TryGet(serializedPokemon.HeldItem, out var item))
                throw new KeyNotFoundException($"Item {serializedPokemon.HeldItem} not found.");
            HeldItem = item;
        }

        CurrentHealth = serializedPokemon.CurrentHealth;
        HeightInMeters = form.Height;
        Happiness = serializedPokemon.Happiness;
        IndividualValues = serializedPokemon.IndividualValues.ToIndividualValueStatisticSet();
        EffortValues = serializedPokemon.EffortValues.ToEffortValueStatisticSet();
        if (!library.StaticLibrary.Natures.TryGet(serializedPokemon.Nature, out var nature))
            throw new KeyNotFoundException($"Nature {serializedPokemon.Nature} not found.");
        Nature = nature;
        Nickname = serializedPokemon.Nickname;
        if (!library.StaticLibrary.Abilities.TryGet(serializedPokemon.Ability, out var ability))
            throw new KeyNotFoundException($"Ability {serializedPokemon.Ability} not found.");
        AbilityIndex = form.FindAbilityIndex(ability) ??
                       throw new KeyNotFoundException(
                           $"Ability {ability.Name} not found on species {species.Name} form {form.Name}.");
        Volatile = new ScriptSet(this);
        _learnedMoves = serializedPokemon.Moves.Select(move =>
        {
            if (move == null)
                return null;
            if (!library.StaticLibrary.Moves.TryGet(move.MoveName, out var moveData))
                throw new KeyNotFoundException($"Move {move.MoveName} not found");
            return (ILearnedMove)new LearnedMoveImpl(moveData, move.LearnMethod, move.CurrentPp);
        }).ToArray();
        AllowedExperience = serializedPokemon.AllowedExperience;
        IsEgg = serializedPokemon.IsEgg;
        Types = form.Types;
        RecalculateFlatStats();

        if (serializedPokemon.Status != null)
        {
            if (!library.ScriptResolver.TryResolve(ScriptCategory.Status, serializedPokemon.Status, null,
                    out var statusScript))
                throw new KeyNotFoundException($"Status script {serializedPokemon.Status} not found");
            StatusScript.Set(statusScript);
            statusScript.OnAddedToParent(this);
        }
    }

    /// <inheritdoc />
    public IDynamicLibrary Library { get; }

    /// <inheritdoc />
    public ISpecies Species { get; private set; }

    /// <inheritdoc />
    public IForm Form { get; private set; }

    /// <inheritdoc />
    public ISpecies? DisplaySpecies { get; set; }

    /// <inheritdoc />
    public IForm? DisplayForm { get; set; }

    /// <inheritdoc />
    public void SetDisplaySpecies(ISpecies? species, IForm? form)
    {
        DisplaySpecies = species;
        DisplayForm = form;

        BattleData?.Battle.EventHook.Invoke(new DisplaySpeciesChangeEvent(this, species, form)
        {
            BatchId = new EventBatchId(),
        });
    }

    /// <inheritdoc />
    public LevelInt Level { get; private set; }

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

    /// <inheritdoc />
    public bool AddExperience(uint experience)
    {
        if (!AllowedExperience)
            return false;
        var maxLevel = Library.StaticLibrary.Settings.MaxLevel;
        if (Level >= maxLevel)
            return false;
        var oldLevel = Level;
        var oldExperience = Experience;
        Experience += experience;
        var batchId = new EventBatchId();
        BattleData?.Battle.EventHook.Invoke(new ExperienceGainEvent(this, oldExperience, Experience)
        {
            BatchId = batchId,
        });

        var newLevel = Library.StaticLibrary.GrowthRates.CalculateLevel(Species.GrowthRate, Experience);
        if (newLevel > Level)
        {
            Level = newLevel;
            RecalculateFlatStats();
            BattleData?.Battle.EventHook.Invoke(new LevelUpEvent(this, oldLevel, Level)
            {
                BatchId = batchId,
            });

            if (newLevel >= maxLevel)
            {
                Experience = Library.StaticLibrary.GrowthRates.CalculateExperience(Species.GrowthRate, maxLevel);
            }
        }

        return oldExperience != Experience;
    }

    /// <inheritdoc />
    public uint PersonalityValue { get; }

    /// <inheritdoc />
    public Gender Gender { get; private set; }

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

    /// <inheritdoc />
    public bool IsShiny => Coloring == 1;

    /// <inheritdoc />
    public IItem? HeldItem { get; private set; }

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

    /// <inheritdoc />
    public float WeightInKg
    {
        get
        {
            var weight = Form.Weight;
            if (BattleData is not null)
                // ReSharper disable once AccessToModifiedClosure
                this.RunScriptHook<IScriptModifyWeight>(script => script.ModifyWeight(ref weight));
            if (weight < 0.1f)
                weight = 0.1f;
            return weight;
        }
    }

    /// <inheritdoc />
    public float HeightInMeters { get; set; }

    /// <inheritdoc />
    public byte Happiness { get; set; }

    /// <inheritdoc />
    public StatisticSet<uint> FlatStats { get; } = new();

    /// <inheritdoc />
    public StatBoostStatisticSet StatBoost { get; } = new();

    /// <inheritdoc />
    public StatisticSet<uint> BoostedStats { get; } = new();

    /// <inheritdoc />
    public uint MaxHealth => BoostedStats.Hp;

    /// <inheritdoc />
    public IndividualValueStatisticSet IndividualValues { get; } = new();

    /// <inheritdoc />
    public EffortValueStatisticSet EffortValues { get; } = new();

    /// <inheritdoc />
    public INature Nature { get; }

    /// <inheritdoc />
    public string? Nickname { get; set; }

    /// <inheritdoc />
    public AbilityIndex AbilityIndex { get; }

    /// <inheritdoc />
    public IAbility? OverrideAbility { get; private set; }

    /// <inheritdoc />
    public IPokemonBattleData? BattleData { get; private set; }

    private readonly ILearnedMove?[] _learnedMoves = new ILearnedMove[Const.MovesCount];

    /// <inheritdoc />
    public IReadOnlyList<ILearnedMove?> Moves => _learnedMoves;

    /// <inheritdoc />
    public bool HasMove(StringKey moveName) => Moves.Any(move => move?.MoveData.Name == moveName);

    /// <inheritdoc />
    public void SwapMoves(byte index1, byte index2)
    {
        if (index1 >= Const.MovesCount || index2 >= Const.MovesCount)
            return;
        var move1 = _learnedMoves[index1];
        var move2 = _learnedMoves[index2];
        _learnedMoves[index1] = move2;
        _learnedMoves[index2] = move1;
    }

    /// <inheritdoc />
    public bool AllowedExperience { get; set; }

    private List<TypeIdentifier> _types = new();

    /// <inheritdoc />
    public IReadOnlyList<TypeIdentifier> Types
    {
        get => _types;
        private set => _types = value.ToList();
    }

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

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

    /// <inheritdoc />
    public void MarkAsCaught()
    {
        IsCaught = true;
    }

    /// <inheritdoc />
    public ScriptContainer HeldItemTriggerScript { get; } = new();

    /// <inheritdoc />
    public ScriptContainer AbilityScript { get; } = new();

    /// <inheritdoc />
    public ScriptContainer StatusScript { get; } = new();

    /// <inheritdoc />
    public int? GetStatusTurnsLeft => (StatusScript.Script as IAIInfoScriptNumberTurnsLeft)?.TurnsLeft();

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

    /// <inheritdoc />
    public bool HasHeldItem(StringKey itemName) => HeldItem?.Name == itemName;

    /// <inheritdoc />
    public IItem? ForceSetHeldItem(IItem? item)
    {
        var previous = HeldItem;
        HeldItem = item;
        this.RunScriptHook<IScriptOnAfterHeldItemChange>(x => x.OnAfterHeldItemChange(this, previous, item));
        return previous;
    }

    /// <inheritdoc />
    public IItem? RemoveHeldItem()
    {
        if (HeldItem is not null)
        {
            if (HeldItem.Category == ItemCategory.FormChanger)
            {
                return null;
            }
        }
        var previous = HeldItem;
        HeldItem = null;
        this.RunScriptHook<IScriptOnAfterHeldItemChange>(x => x.OnAfterHeldItemChange(this, previous, null));
        return previous;
    }

    /// <inheritdoc />
    public bool HasItemBeenRemovedForBattle => _removedHeldItem is not null;

    private IItem? _removedHeldItem;

    /// <inheritdoc />
    public IItem? RemoveHeldItemForBattle()
    {
        return _removedHeldItem = RemoveHeldItem();
    }

    /// <inheritdoc />
    public bool TryStealHeldItem([NotNullWhen(true)] out IItem? item)
    {
        if (HeldItem is null || HeldItem.Category == ItemCategory.FormChanger)
        {
            item = null;
            return false;
        }
        var prevent = false;
        this.RunScriptHook<IScriptPreventHeldItemSteal>(script =>
            script.PreventHeldItemSteal(this, HeldItem, ref prevent));
        if (prevent)
        {
            item = null;
            return false;
        }
        item = RemoveHeldItemForBattle();
        return item is not null;
    }

    /// <inheritdoc />
    public void RestoreRemovedHeldItem()
    {
        _ = ForceSetHeldItem(_removedHeldItem);
        _removedHeldItem = null;
    }

    /// <inheritdoc />
    public bool ConsumeHeldItem()
    {
        if (HeldItem is null)
            return false;
        if (!Library.ScriptResolver.TryResolveBattleItemScript(HeldItem, out _))
            return false;

        if (BattleData != null)
        {
            var prevented = false;
            this.RunScriptHook<IScriptPreventHeldItemConsume>(script =>
                script.PreventHeldItemConsume(this, HeldItem, ref prevented));
            if (prevented)
                return false;
            BattleData.MarkItemAsConsumed(HeldItem);
        }

        UseItem(ForceSetHeldItem(null)!);
        return true;
    }

    /// <summary>
    /// Uses an item on this Pokémon.
    /// </summary>
    /// <param name="item"></param>
    public void UseItem(IItem item)
    {
        // TODO: actually consume the item

        this.RunScriptHook<IScriptOnAfterItemConsume>(x => x.OnAfterItemConsume(this, item));
    }

    /// <inheritdoc />
    public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force,
        EventBatchId batchId = default)
    {
        if (!force)
        {
            var prevented = false;
            this.RunScriptHook<IScriptPreventStatBoostChange>(script =>
                script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented));
            if (prevented)
                return false;
            this.RunScriptHook<IScriptChangeStatBoostChange>(script =>
                script.ChangeStatBoostChange(this, stat, selfInflicted, ref change));
            if (change == 0)
                return false;
        }
        var changed = false;
        var oldBoost = StatBoost.GetStatistic(stat);
        changed = change switch
        {
            > 0 => StatBoost.IncreaseStatistic(stat, change),
            < 0 => StatBoost.DecreaseStatistic(stat, (sbyte)-change),
            _ => changed,
        };
        if (!changed)
            return false;
        if (BattleData != null)
        {
            var newBoost = StatBoost.GetStatistic(stat);
            BattleData.Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost)
            {
                BatchId = batchId,
            });
        }

        RecalculateBoostedStats();
        this.RunScriptHook<IScriptOnAfterStatBoostChange>(script =>
            script.OnAfterStatBoostChange(this, stat, selfInflicted, change));
        return true;
    }

    /// <summary>
    /// Whether the ability of the Pokémon is suppressed.
    /// </summary>
    public bool AbilitySuppressed { get; private set; }

    /// <inheritdoc />
    public bool SuppressAbility()
    {
        if (ActiveAbility?.CanBeChanged == false)
            return false;

        AbilitySuppressed = true;
        AbilityScript.Clear();
        return true;
    }

    private (IAbility, AbilityIndex)? _abilityCache;

    /// <inheritdoc />
    public IAbility? ActiveAbility
    {
        get
        {
            if (AbilitySuppressed)
                return null;
            if (OverrideAbility != null)
                return OverrideAbility;
            if (_abilityCache is not null && _abilityCache.Value.Item2 == AbilityIndex)
                return _abilityCache.Value.Item1;
            var ability = Form.GetAbility(AbilityIndex);
            if (!Library.StaticLibrary.Abilities.TryGet(ability, out var abilityObj))
                throw new KeyNotFoundException($"Ability {ability} not found.");
            _abilityCache = (abilityObj, AbilityIndex);
            return abilityObj;
        }
    }

    /// <inheritdoc />
    public void RecalculateFlatStats()
    {
        Library.StatCalculator.CalculateFlatStats(this, FlatStats);
        RecalculateBoostedStats();
    }

    /// <inheritdoc />
    public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats);

    /// <inheritdoc />
    public bool EvolveTo(IEvolution evolution, EventHook? eventHook = null)
    {
        if (!Species.EvolutionData.Contains(evolution))
            return false;
        if (!Library.StaticLibrary.Species.TryGet(evolution.ToSpecies, out var species))
            return false;

        // TODO: Consider how forms work with evolution items.
        var newForm = species.GetDefaultForm();
        eventHook?.Invoke(new EvolutionEvent(this, Species, Form, species, newForm, evolution));

        ChangeSpecies(species, newForm, eventHook);
        return true;
    }

    /// <inheritdoc />
    public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null)
    {
        eventHook ??= BattleData?.Battle.EventHook;
        if (Species == species)
        {
            if (form != Form)
                ChangeForm(form, new EventBatchId());
            return;
        }

        // If the Pokémon is genderless, but its new species is not, we want to set its gender
        if (Gender != Gender.Genderless && species.GenderRate < 0.0)
        {
            var random = (IRandom?)BattleData?.Battle.Random ?? new RandomImpl();
            Gender = species.GetRandomGender(random);
        }
        // Else if the new species is genderless, but the Pokémon has a gender, make the creature genderless.
        else if (species.GenderRate < 0.0 && Gender != Gender.Genderless)
        {
            Gender = Gender.Genderless;
        }

        var batchId = new EventBatchId();
        eventHook?.Invoke(new SpeciesChangeEvent(this, species, form)
        {
            BatchId = batchId,
        });
        Species = species;
        ChangeForm(form, batchId);
    }

    /// <inheritdoc />
    public void ChangeForm(IForm form, EventBatchId batchId = default, EventHook? eventHook = null)
    {
        if (form == Form)
            return;
        eventHook ??= BattleData?.Battle.EventHook;

        var oldAbility = Form.GetAbility(AbilityIndex);

        Form = form;
        Types = form.Types.ToList();
        HeightInMeters = form.Height;
        var abilityIndex = AbilityIndex;
        abilityIndex = AbilityIndex.IsHidden switch
        {
            true when form.HiddenAbilities.Count <= abilityIndex.Index => new AbilityIndex
            {
                IsHidden = true, Index = (byte)(form.HiddenAbilities.Count - 1),
            },
            false when form.Abilities.Count <= abilityIndex.Index => new AbilityIndex
            {
                IsHidden = false, Index = (byte)(form.Abilities.Count - 1),
            },
            _ => abilityIndex,
        };

        var newAbility = Form.GetAbility(abilityIndex);

        if (OverrideAbility == null && oldAbility != newAbility)
        {
            AbilityScript.Clear();
            if (!Library.StaticLibrary.Abilities.TryGet(newAbility, out var ability))
                throw new KeyNotFoundException($"Ability {newAbility} not found.");
            if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, newAbility, ability.Parameters,
                    out var abilityScript))
            {
                AbilityScript.Set(abilityScript);
                abilityScript.OnAddedToParent(this);
            }
            else
            {
                AbilityScript.Clear();
            }
        }

        var oldHealth = BoostedStats.Hp;
        RecalculateFlatStats();
        var diffHealth = (long)BoostedStats.Hp - oldHealth;
        if (diffHealth > 0)
        {
            Heal((uint)diffHealth, true);
        }

        // TODO: form specific moves?

        eventHook?.Invoke(new FormChangeEvent(this, form)
        {
            BatchId = batchId,
        });
    }

    /// <inheritdoc />
    /// <remarks>
    /// Currently this checks the Pokémon is not an egg, not caught, and not fainted.
    /// </remarks>
    public bool IsUsable => !IsCaught && !IsEgg && !IsFainted;

    /// <inheritdoc />
    public bool IsFainted => CurrentHealth == 0;

    /// <inheritdoc />
    public void Damage(uint damage, DamageSource source, EventBatchId batchId, bool forceDamage = false)
    {
        // If the Pokémon is already fainted, we don't need to do anything.
        if (IsFainted)
            return;
        if (BattleData is not null && !forceDamage)
        {
            var dmg = damage;
            this.RunScriptHook<IScriptChangeIncomingDamage>(script =>
                script.ChangeIncomingDamage(this, source, ref dmg));
            damage = dmg;
        }
        if (damage == 0)
            return;

        // If the damage is more than the current health, we cap it at the current health, to prevent
        // underflow.
        if (damage >= CurrentHealth)
            damage = CurrentHealth;
        // Calculate the new health.
        var newHealth = CurrentHealth - damage;
        if (BattleData is not null)
        {
            // If the Pokémon is in a battle, we trigger an event to the front-end.
            BattleData.Battle.EventHook.Invoke(new DamageEvent(this, CurrentHealth, newHealth, source)
            {
                BatchId = batchId,
            });
            // And allow scripts to execute.
            this.RunScriptHook<IScriptOnDamage>(script => script.OnDamage(this, source, CurrentHealth, newHealth));
        }

        CurrentHealth = newHealth;

        // If the Pokémon is now fainted, we also run faint handling.
        if (IsFainted)
        {
            OnFaint(source);
        }
    }

    /// <inheritdoc />
    public void Faint(DamageSource source, EventBatchId batchId = default)
    {
        CurrentHealth = 0;
        OnFaint(source);
    }

    private void OnFaint(DamageSource source)
    {
        // If the Pokémon is not in a battle, we don't need to do anything.
        if (BattleData is null)
            return;

        // Trigger the faint event to the front-end.
        BattleData.Battle.EventHook.Invoke(new FaintEvent(this));

        // Allow scripts to trigger based on the faint.
        this.RunScriptHook<IScriptOnFaint>(script => script.OnFaint(this, source));
        foreach (var ally in BattleData.BattleSide.Pokemon.WhereNotNull().Where(x => x != this))
        {
            ally.RunScriptHook<IScriptOnAllyFaint>(script => script.OnAllyFaint(ally, this));
        }

        // Make sure the OnRemove script is run.
        this.RunScriptHook<IScriptOnRemove>(script => script.OnRemove());

        // Mark the position as unfillable if it can't be filled by any party.
        if (!BattleData.Battle.CanSlotBeFilled(BattleData.SideIndex, BattleData.Position))
        {
            BattleData.Battle.Sides[BattleData.SideIndex].MarkPositionAsUnfillable(BattleData.Position);
        }
        BattleData.BattleSide.MarkFaint(BattleData.Position);
        BattleData.BattleSide.ForceClearPokemonFromField(BattleData.Position);

        foreach (var opponent in BattleData.SeenOpponents.WhereNotNull())
        {
            if (!opponent.IsUsable)
                continue;
            if (!opponent.AllowedExperience)
                continue;
            opponent.AddExperience(Library.ExperienceGainCalculator.CalculateExperienceGain(this, opponent));
        }

        // Validate the battle state to see if the battle is over.
        BattleData.Battle.ValidateBattleState();
    }

    /// <inheritdoc />
    public bool Heal(uint heal, bool allowRevive, EventBatchId batchId = default, bool forceHeal = false,
        EventHook? customEventHook = null)
    {
        if (IsFainted && !allowRevive)
            return false;

        var maxAmount = BoostedStats.Hp - CurrentHealth;
        if (heal > maxAmount)
            heal = maxAmount;
        if (heal == 0)
            return false;
        if (!forceHeal)
        {
            var prevented = false;
            this.RunScriptHook<IScriptPreventHeal>(x => x.PreventHeal(this, heal, allowRevive, ref prevented));
            if (prevented)
                return false;
        }

        var newHealth = CurrentHealth + heal;
        customEventHook ??= BattleData?.Battle.EventHook;
        customEventHook?.Invoke(new HealEvent(this, CurrentHealth, newHealth)
        {
            BatchId = batchId,
        });
        CurrentHealth = newHealth;
        return true;
    }

    /// <inheritdoc />
    public void RestoreAllPP()
    {
        foreach (var move in Moves)
        {
            move?.RestoreAllUses();
        }
    }

    /// <inheritdoc />
    /// <remarks>
    /// If the index is 255, it will try to find the first empty move slot.
    /// </remarks>
    public void LearnMove(StringKey moveName, MoveLearnMethod method, byte index)
    {
        if (index == 255)
        {
            for (byte i = 0; i < Moves.Count; i++)
            {
                if (Moves[i] is not null)
                    continue;
                index = i;
                break;
            }
        }

        if (index >= Moves.Count)
            throw new InvalidOperationException("No empty move slot found.");
        if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move))
            throw new KeyNotFoundException($"Move {moveName} not found.");

        _learnedMoves[index] = new LearnedMoveImpl(move, method);
    }

    /// <inheritdoc />
    public bool HasStatus(StringKey status) => StatusScript.Script?.Name == status;

    /// <inheritdoc />
    public bool SetStatus(StringKey status, IPokemon? originPokemon, EventBatchId batchId = default)
    {
        if (!Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out var statusScript))
            throw new KeyNotFoundException($"Status script {status} not found");

        if (!StatusScript.IsEmpty)
            return false;
        var oldStatus = StatusScript.Script?.Name;
        var selfInflicted = originPokemon == this;

        var preventStatus = false;
        this.RunScriptHook<IScriptPreventStatusChange>(script =>
            script.PreventStatusChange(this, status, selfInflicted, ref preventStatus));
        if (preventStatus)
            return false;

        StatusScript.Set(statusScript);
        statusScript.OnAddedToParent(this);
        BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, status)
        {
            BatchId = batchId,
        });
        this.RunScriptHook<IScriptOnAfterStatusChange>(script =>
            script.OnAfterStatusChange(this, status, originPokemon));
        return true;
    }

    /// <inheritdoc />
    public void ClearStatus(EventBatchId batchId = default)
    {
        StatusScript.Clear();
        BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, StatusScript.Script?.Name, null)
        {
            BatchId = batchId,
        });
    }

    /// <inheritdoc />
    public void ChangeLevelBy(int change)
    {
        var newLevel = Level + change;
        Level = (LevelInt)Math.Clamp(newLevel, 1, Library.StaticLibrary.Settings.MaxLevel);
        RecalculateFlatStats();
    }

    /// <inheritdoc />
    public void SetBattleData(IBattle battle, byte sideIndex)
    {
        if (BattleData is not null)
        {
            BattleData.SetBattle(battle, sideIndex, battle.CurrentTurnNumber);
        }
        else
        {
            BattleData = new PokemonBattleDataImpl(battle, sideIndex, battle.CurrentTurnNumber, Species, Form);
        }
        if (ActiveAbility != null && Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ActiveAbility.Name,
                ActiveAbility.Parameters, out var abilityScript))
        {
            AbilityScript.Set(abilityScript);
            abilityScript.OnAddedToParent(this);
        }
        else
        {
            AbilityScript.Clear();
        }
    }

    /// <inheritdoc />
    public void SetOnBattlefield(bool onBattleField)
    {
        if (BattleData is not null)
        {
            BattleData.SetOnBattlefield(onBattleField);
            if (!onBattleField)
            {
                Volatile.Clear();
                HeightInMeters = Form.Height;
                Types = Form.Types;
                OverrideAbility = null;
                AbilitySuppressed = false;
                RecalculateFlatStats();
            }
        }
    }

    /// <inheritdoc />
    public void SetBattleSidePosition(byte position)
    {
        BattleData?.SetPosition(position);
    }

    /// <inheritdoc />
    public void ClearBattleData()
    {
        var battleData = BattleData;
        BattleData = null;
        Volatile.Clear();
        HeightInMeters = Form.Height;
        Types = Form.Types;
        OverrideAbility = null;
        AbilitySuppressed = false;
        StatBoost.Reset();
        if (battleData != null && Form.IsBattleOnlyForm)
        {
            ChangeForm(battleData.OriginalSpecies == Species ? battleData.OriginalForm : Species.GetDefaultForm());
        }
        DisplaySpecies = null;
        DisplayForm = null;
    }

    /// <inheritdoc />
    public void MarkOpponentAsSeen(IPokemon pokemon) => BattleData?.MarkOpponentAsSeen(pokemon);

    /// <inheritdoc />
    public bool RemoveType(TypeIdentifier type) => _types.Remove(type);

    /// <inheritdoc />
    public bool AddType(TypeIdentifier type)
    {
        if (_types.Contains(type))
            return false;
        _types.Add(type);
        return true;
    }

    /// <inheritdoc />
    public void SetTypes(IReadOnlyList<TypeIdentifier> types)
    {
        _types = types.ToList();
    }

    /// <inheritdoc />
    public bool ChangeAbility(IAbility ability)
    {
        if (!ability.CanBeChanged)
            return false;
        OverrideAbility = ability;
        if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters,
                out var abilityScript))
        {
            AbilityScript.Set(abilityScript);
            abilityScript.OnAddedToParent(this);
        }
        else
        {
            AbilityScript.Clear();
        }
        return true;
    }

    /// <inheritdoc />
    public bool IsFloating
    {
        get
        {
            var isFloating = Types.Any(x => x.Name == "flying");
            this.RunScriptHook<IScriptIsFloating>(x => x.IsFloating(this, ref isFloating));
            return isFloating;
        }
    }

    /// <inheritdoc />
    public SerializedPokemon Serialize() => new(this);

    /// <inheritdoc />
    public override int ScriptCount
    {
        get
        {
            var c = 4;
            if (BattleData != null)
            {
                var side = BattleData.Battle.Sides[BattleData.SideIndex];
                c += side.ScriptCount;
            }

            return c;
        }
    }

    /// <inheritdoc />
    public override void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts)
    {
        scripts.Add(HeldItemTriggerScript);
        scripts.Add(AbilityScript);
        scripts.Add(StatusScript);
        scripts.Add(Volatile);
    }

    /// <inheritdoc />
    public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts)
    {
        GetOwnScripts(scripts);
        if (BattleData != null)
        {
            var side = BattleData.Battle.Sides[BattleData.SideIndex];
            side.CollectScripts(scripts);
        }
    }

    /// <inheritdoc />
    public override string ToString()
    {
        if (!string.IsNullOrEmpty(Nickname))
            return $"{Nickname} ({Species.Name})";
        return Species.Name;
    }
}

/// <inheritdoc />
public class PokemonBattleDataImpl : IPokemonBattleData
{
    /// <inheritdoc cref="PokemonBattleDataImpl"/>
    public PokemonBattleDataImpl(IBattle battle, byte sideIndex, uint switchInTurn, ISpecies originalSpecies,
        IForm originalForm)
    {
        Battle = battle;
        SideIndex = sideIndex;
        SwitchInTurn = switchInTurn;
        OriginalSpecies = originalSpecies;
        OriginalForm = originalForm;
    }

    /// <inheritdoc />
    public void SetBattle(IBattle battle, byte sideIndex, uint switchInTurn)
    {
        Battle = battle;
        SideIndex = sideIndex;
        SwitchInTurn = switchInTurn;
    }

    /// <inheritdoc />
    public void SetPosition(byte position)
    {
        Position = position;
    }

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

    /// <inheritdoc />
    public byte SideIndex { get; set; }

    /// <inheritdoc />
    public byte Position { get; set; }

    private readonly List<IPokemon> _seenOpponents = [];

    /// <inheritdoc />
    public IReadOnlyList<IPokemon> SeenOpponents => _seenOpponents;

    /// <inheritdoc />
    public void SetOnBattlefield(bool onBattleField)
    {
        IsOnBattlefield = onBattleField;
    }

    /// <inheritdoc />
    public bool IsOnBattlefield { get; set; }

    /// <inheritdoc />
    public void MarkOpponentAsSeen(IPokemon opponent)
    {
        _seenOpponents.Add(opponent);
    }

    private readonly List<IItem> _consumedItems = [];

    /// <inheritdoc />
    public IReadOnlyList<IItem> ConsumedItems => _consumedItems;

    /// <inheritdoc />
    public void MarkItemAsConsumed(IItem item)
    {
        _consumedItems.Add(item);
        BattleSide.SetConsumedItem(Position, item);
    }

    /// <inheritdoc />
    public uint SwitchInTurn { get; set; }

    /// <inheritdoc />
    public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn;

    /// <inheritdoc />
    public IBattleSide BattleSide => Battle.Sides[SideIndex];

    /// <inheritdoc />
    public ISpecies OriginalSpecies { get; }

    /// <inheritdoc />
    public IForm OriginalForm { get; }

    /// <inheritdoc />
    public IMoveChoice? LastMoveChoice { get; set; }
}