using System.Collections.Immutable;
using PkmnLib.Static.Utils;
using PkmnLib.Static.Utils.Errors;

namespace PkmnLib.Static.Species;

/// <summary>
/// A form is a variant of a specific species. A species always has at least one form, but can have
/// many more.
/// </summary>
public interface IForm : INamedValue
{
    /// <summary>
    /// The height of the form in meters.
    /// </summary>
    float Height { get; }

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

    /// <summary>
    /// The base amount of experience that is gained when beating a Pokémon with this form.
    /// </summary>
    uint BaseExperience { get; }

    /// <summary>
    /// The normal types a Pokémon with this form has.
    /// </summary>
    IReadOnlyList<TypeIdentifier> Types { get; }

    /// <summary>
    /// The inherent values of a form of species that are used for the stats of a Pokémon.
    /// </summary>
    ImmutableStatisticSet<ushort> BaseStats { get; }

    /// <summary>
    /// The possible abilities a Pokémon with this form can have.
    /// </summary>
    IReadOnlyList<StringKey> Abilities { get; }

    /// <summary>
    /// The possible hidden abilities a Pokémon with this form can have.
    /// </summary>
    IReadOnlyList<StringKey> HiddenAbilities { get; }

    /// <summary>
    /// The moves a Pokémon with this form can learn.
    /// </summary>
    ILearnableMoves Moves { get; }

    /// <summary>
    /// Arbitrary flags can be set on a form for scripting use.
    /// </summary>
    ImmutableHashSet<StringKey> Flags { get; }

    /// <summary>
    /// Get a type of the form at a certain index.
    /// </summary>
    TypeIdentifier GetType(int index);

    /// <summary>
    /// Gets a single base stat value.
    /// </summary>
    ushort GetBaseStat(Statistic stat);

    /// <summary>
    /// Find the index of an ability that can be on this form.
    /// </summary>
    AbilityIndex? FindAbilityIndex(IAbility ability);

    /// <summary>
    /// Gets an ability from the form.
    /// </summary>
    StringKey GetAbility(AbilityIndex index);

    /// <summary>
    /// Gets a random ability from the form.
    /// </summary>
    StringKey GetRandomAbility(IRandom rand);

    /// <summary>
    /// Gets a random hidden ability from the form.
    /// </summary>
    StringKey GetRandomHiddenAbility(IRandom rand);

    /// <summary>
    /// Check if the form has a specific flag set.
    /// </summary>
    bool HasFlag(StringKey key);

    /// <summary>
    /// Check if the form is a battle-only form, meaning it should return to its original form after the battle ends.
    /// </summary>
    bool IsBattleOnlyForm { get; }
}

/// <inheritdoc />
public class FormImpl : IForm
{
    /// <inheritdoc cref="FormImpl" />
    public FormImpl(StringKey name, float height, float weight, uint baseExperience, IEnumerable<TypeIdentifier> types,
        ImmutableStatisticSet<ushort> baseStats, IEnumerable<StringKey> abilities,
        IEnumerable<StringKey> hiddenAbilities, ILearnableMoves moves, ImmutableHashSet<StringKey> flags,
        bool isBattleOnlyForm)
    {
        Name = name;
        Height = height;
        Weight = weight;
        BaseExperience = baseExperience;
        Types = [..types];
        BaseStats = baseStats;
        Abilities = [..abilities];
        HiddenAbilities = [..hiddenAbilities];
        Moves = moves;
        Flags = flags;
        IsBattleOnlyForm = isBattleOnlyForm;

        if (Types.Count == 0)
            throw new ArgumentException("A form must have at least one type.");
        if (Abilities.Count == 0)
            throw new ArgumentException("A form must have at least one ability.");
    }

    /// <inheritdoc />
    public StringKey Name { get; }

    /// <inheritdoc />
    public float Height { get; }

    /// <inheritdoc />
    public float Weight { get; }

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

    /// <inheritdoc />
    public IReadOnlyList<TypeIdentifier> Types { get; }

    /// <inheritdoc />
    public ImmutableStatisticSet<ushort> BaseStats { get; }

    /// <inheritdoc />
    public IReadOnlyList<StringKey> Abilities { get; }

    /// <inheritdoc />
    public IReadOnlyList<StringKey> HiddenAbilities { get; }

    /// <inheritdoc />
    public ILearnableMoves Moves { get; }

    /// <inheritdoc />
    public ImmutableHashSet<StringKey> Flags { get; }

    /// <inheritdoc />
    public TypeIdentifier GetType(int index)
    {
        if (index < 0 || index >= Types.Count)
            throw new OutOfRangeException("Type", index, Types.Count);
        return Types[index];
    }

    /// <inheritdoc />
    public ushort GetBaseStat(Statistic stat) => BaseStats.GetStatistic(stat);

    /// <inheritdoc />
    public AbilityIndex? FindAbilityIndex(IAbility ability)
    {
        for (var i = 0; i < Abilities.Count && i < 255; i++)
        {
            if (Abilities[i] == ability.Name)
            {
                return new AbilityIndex
                {
                    IsHidden = false,
                    Index = (byte)i,
                };
            }
        }
        for (var i = 0; i < HiddenAbilities.Count && i < 255; i++)
        {
            if (HiddenAbilities[i] == ability.Name)
            {
                return new AbilityIndex
                {
                    IsHidden = true,
                    Index = (byte)i,
                };
            }
        }
        return null;
    }

    /// <inheritdoc />
    public StringKey GetAbility(AbilityIndex index)
    {
        var array = index.IsHidden ? HiddenAbilities : Abilities;
        if (index.Index >= array.Count)
            throw new OutOfRangeException("Ability", index.Index, array.Count);
        return array[index.Index];
    }

    /// <inheritdoc />
    public StringKey GetRandomAbility(IRandom rand) => Abilities[rand.GetInt(Abilities.Count)];

    /// <inheritdoc />
    public StringKey GetRandomHiddenAbility(IRandom rand) => HiddenAbilities[rand.GetInt(HiddenAbilities.Count)];

    /// <inheritdoc />
    public bool HasFlag(StringKey key) => Flags.Contains(key);

    /// <inheritdoc />
    public bool IsBattleOnlyForm { get; }
}