using System.Collections;
using System.Diagnostics.CodeAnalysis;
using PkmnLib.Static.Utils;

namespace PkmnLib.Static;

/// <summary>
/// A set of statistics that cannot be changed.
/// </summary>
/// <typeparam name="T">The size of the integer to be used</typeparam>
public record ImmutableStatisticSet<T> where T : struct
{
    /// <summary>
    /// The health points stat value.
    /// </summary>
    public T Hp { get; protected set; }

    /// <summary>
    /// The physical attack stat value.
    /// </summary>
    public T Attack { get; protected set; }

    /// <summary>
    /// The physical defense stat value.
    /// </summary>
    public T Defense { get; protected set; }

    /// <summary>
    /// The special attack stat value.
    /// </summary>
    public T SpecialAttack { get; protected set; }

    /// <summary>
    /// The special defense stat value.
    /// </summary>
    public T SpecialDefense { get; protected set; }

    /// <summary>
    /// The speed stat value.
    /// </summary>
    public T Speed { get; protected set; }

    /// <inheritdoc cref="ImmutableStatisticSet{T}"/>
    public ImmutableStatisticSet(T hp, T attack, T defense, T specialAttack, T specialDefense, T speed)
    {
        Hp = hp;
        Attack = attack;
        Defense = defense;
        SpecialAttack = specialAttack;
        SpecialDefense = specialDefense;
        Speed = speed;
    }

    /// <inheritdoc cref="ImmutableStatisticSet{T}"/>
    public ImmutableStatisticSet(ImmutableStatisticSet<T> set)
    {
        Hp = set.Hp;
        Attack = set.Attack;
        Defense = set.Defense;
        SpecialAttack = set.SpecialAttack;
        SpecialDefense = set.SpecialDefense;
        Speed = set.Speed;
    }

    /// <summary>
    /// Gets a statistic from the set.
    /// </summary>
    public virtual T GetStatistic(Statistic stat)
    {
        return stat switch
        {
            Statistic.Hp => Hp,
            Statistic.Attack => Attack,
            Statistic.Defense => Defense,
            Statistic.SpecialAttack => SpecialAttack,
            Statistic.SpecialDefense => SpecialDefense,
            Statistic.Speed => Speed,
            _ => throw new ArgumentException($"Invalid statistic {stat}"),
        };
    }
}

/// <summary>
/// A set of statistics that can be changed.
/// </summary>
/// <typeparam name="T"></typeparam>
public record StatisticSet<T> : ImmutableStatisticSet<T>, IEnumerable<(Statistic statistic, T value)>, IDeepCloneable
    where T : struct
{
    /// <inheritdoc cref="StatisticSet{T}"/>
    public StatisticSet() : base(default, default, default, default, default, default)
    {
    }

    /// <inheritdoc cref="StatisticSet{T}"/>
    public StatisticSet(T hp, T attack, T defense, T specialAttack, T specialDefense, T speed) : base(hp, attack,
        defense, specialAttack, specialDefense, speed)
    {
    }

    /// <inheritdoc cref="StatisticSet{T}"/>
    public StatisticSet(StatisticSet<T> set) : base(set)
    {
    }

    /// <summary>
    /// Helper function to add two numerics together.
    /// </summary>
    protected T Add(T a, T b) => (T)Convert.ChangeType(Convert.ToInt32(a) + Convert.ToInt32(b), typeof(T));

    /// <summary>
    /// Helper function to subtract two numerics.
    /// </summary>
    protected T Subtract(T a, T b) => (T)Convert.ChangeType(Convert.ToInt32(a) - Convert.ToInt32(b), typeof(T));

    /// <summary>
    /// Modifies a statistic in the set.
    /// </summary>
    public virtual void SetStatistic(Statistic stat, T value)
    {
        switch (stat)
        {
            case Statistic.Hp:
                Hp = value;
                break;
            case Statistic.Attack:
                Attack = value;
                break;
            case Statistic.Defense:
                Defense = value;
                break;
            case Statistic.SpecialAttack:
                SpecialAttack = value;
                break;
            case Statistic.SpecialDefense:
                SpecialDefense = value;
                break;
            case Statistic.Speed:
                Speed = value;
                break;
            default:
                SetUnknownStat(stat, value);
                break;
        }
    }

    /// <summary>
    /// Increases a statistic in the set by a value.
    /// </summary>
    public virtual bool IncreaseStatistic(Statistic stat, T value)
    {
        switch (stat)
        {
            case Statistic.Hp:
                Hp = Add(Hp, value);
                break;
            case Statistic.Attack:
                Attack = Add(Attack, value);
                break;
            case Statistic.Defense:
                Defense = Add(Defense, value);
                break;
            case Statistic.SpecialAttack:
                SpecialAttack = Add(SpecialAttack, value);
                break;
            case Statistic.SpecialDefense:
                SpecialDefense = Add(SpecialDefense, value);
                break;
            case Statistic.Speed:
                Speed = Add(Speed, value);
                break;
            default:
                SetUnknownStat(stat, Add(GetUnknownStat(stat), value));
                break;
        }

        return true;
    }

    /// <summary>
    /// Gets a statistic that is not one of the standard statistics. This can be used for sets where there are
    /// additional statistics, such as evasion or accuracy.
    /// </summary>
    protected virtual T GetUnknownStat(Statistic stat) => throw new ArgumentException($"Invalid statistic {stat}");

    /// <summary>
    /// Sets a statistic that is not one of the standard statistics. This can be used for sets where there are
    /// additional statistics, such as evasion or accuracy.
    /// </summary>
    protected virtual void SetUnknownStat(Statistic stat, T value)
    {
        throw new ArgumentException($"Invalid statistic {stat}");
    }

    /// <summary>
    /// Decreases a statistic in the set by a value.
    /// </summary>
    public virtual bool DecreaseStatistic(Statistic stat, T value)
    {
        switch (stat)
        {
            case Statistic.Hp:
                Hp = Subtract(Hp, value);
                break;
            case Statistic.Attack:
                Attack = Subtract(Attack, value);
                break;
            case Statistic.Defense:
                Defense = Subtract(Defense, value);
                break;
            case Statistic.SpecialAttack:
                SpecialAttack = Subtract(SpecialAttack, value);
                break;
            case Statistic.SpecialDefense:
                SpecialDefense = Subtract(SpecialDefense, value);
                break;
            case Statistic.Speed:
                Speed = Subtract(Speed, value);
                break;
            default:
                SetUnknownStat(stat, Subtract(GetUnknownStat(stat), value));
                break;
        }

        return true;
    }

    /// <inheritdoc />
    public virtual IEnumerator<(Statistic, T)> GetEnumerator()
    {
        yield return (Statistic.Hp, Hp);
        yield return (Statistic.Attack, Attack);
        yield return (Statistic.Defense, Defense);
        yield return (Statistic.SpecialAttack, SpecialAttack);
        yield return (Statistic.SpecialDefense, SpecialDefense);
        yield return (Statistic.Speed, Speed);
    }

    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public bool IsEmpty => Hp.Equals(0) && Attack.Equals(0) && Defense.Equals(0) && SpecialAttack.Equals(0) &&
                           SpecialDefense.Equals(0) && Speed.Equals(0);
}

/// <summary>
/// A set of statistics that can be changed, but are clamped to a minimum and maximum value.
/// </summary>
public abstract record ClampedStatisticSet<T> : StatisticSet<T> where T : struct, IComparable<T>
{
    /// <inheritdoc />
    [SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")]
    protected ClampedStatisticSet(T hp, T attack, T defense, T specialAttack, T specialDefense, T speed) : base(hp,
        attack, defense, specialAttack, specialDefense, speed)
    {
        if (Min.CompareTo(Max) > 0)
            throw new ArgumentException("Minimum value must be less than or equal to maximum value.");
        Hp = Clamp(Hp, Min, Max);
        Attack = Clamp(Attack, Min, Max);
        Defense = Clamp(Defense, Min, Max);
        SpecialAttack = Clamp(SpecialAttack, Min, Max);
        SpecialDefense = Clamp(SpecialDefense, Min, Max);
        Speed = Clamp(Speed, Min, Max);
    }

    /// <inheritdoc cref="ClampedStatisticSet{T}"/>
    protected ClampedStatisticSet(ClampedStatisticSet<T> set) : base(set)
    {
    }

    /// <summary>
    /// Clamps a value to be between the minimum and maximum values.
    /// </summary>
    protected static T Clamp(T value, T min, T max)
    {
        if (value.CompareTo(min) < 0)
            return min;
        if (value.CompareTo(max) > 0)
            return max;

        return value;
    }

    /// <summary>
    /// The minimum value for the statistics.
    /// </summary>
    protected abstract T Min { get; }

    /// <summary>
    /// The maximum value for the statistics.
    /// </summary>
    protected abstract T Max { get; }

    /// <inheritdoc />
    public override void SetStatistic(Statistic stat, T value) => base.SetStatistic(stat, Clamp(value, Min, Max));

    /// <inheritdoc />
    public override bool IncreaseStatistic(Statistic stat, T value)
    {
        var current = GetStatistic(stat);
        var newValue = Add(current, value);
        if (newValue.CompareTo(Max) > 0)
            value = Subtract(Max, current);
        if (value.CompareTo(default) == 0)
            return false;
        return base.IncreaseStatistic(stat, value);
    }

    /// <inheritdoc />
    public override bool DecreaseStatistic(Statistic stat, T value)
    {
        var current = GetStatistic(stat);
        var newValue = Subtract(current, value);
        if (newValue.CompareTo(Min) < 0)
            value = Subtract(current, Min);
        if (value.CompareTo(default) == 0)
            return false;
        return base.DecreaseStatistic(stat, value);
    }
}

/// <summary>
/// A set of statistics that can be changed, but are clamped to a minimum and maximum value of -6 and 6.
/// </summary>
public record StatBoostStatisticSet : ClampedStatisticSet<sbyte>
{
    public const sbyte MaxStatBoost = 6;
    public const sbyte MinStatBoost = -6;

    /// <inheritdoc />
    protected override sbyte Min => -6;

    /// <inheritdoc />
    protected override sbyte Max => 6;

    private sbyte _evasion;

    /// <summary>
    /// The evasion stat value.
    /// </summary>
    public sbyte Evasion
    {
        get => _evasion;
        set => _evasion = Clamp(value, Min, Max);
    }

    private sbyte _accuracy;

    /// <summary>
    /// The accuracy stat value.
    /// </summary>
    public sbyte Accuracy
    {
        get => _accuracy;
        set => _accuracy = Clamp(value, Min, Max);
    }

    /// <inheritdoc cref="StatBoostStatisticSet"/>
    public StatBoostStatisticSet() : base(0, 0, 0, 0, 0, 0)
    {
    }

    /// <inheritdoc cref="StatBoostStatisticSet"/>
    public StatBoostStatisticSet(sbyte hp, sbyte attack, sbyte defense, sbyte specialAttack, sbyte specialDefense,
        sbyte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
    {
    }

    /// <inheritdoc />
    protected override sbyte GetUnknownStat(Statistic stat)
    {
        return stat switch
        {
            Statistic.Evasion => Evasion,
            Statistic.Accuracy => Accuracy,
            _ => throw new ArgumentException($"Invalid statistic {stat}"),
        };
    }

    /// <inheritdoc />
    /// <inheritdoc />
    protected override void SetUnknownStat(Statistic stat, sbyte value)
    {
        switch (stat)
        {
            case Statistic.Evasion:
                Evasion = value;
                break;
            case Statistic.Accuracy:
                Accuracy = value;
                break;
            default:
                throw new ArgumentException($"Invalid statistic {stat}");
        }
    }

    /// <inheritdoc />
    public override IEnumerator<(Statistic, sbyte)> GetEnumerator()
    {
        yield return (Statistic.Hp, Hp);
        yield return (Statistic.Attack, Attack);
        yield return (Statistic.Defense, Defense);
        yield return (Statistic.SpecialAttack, SpecialAttack);
        yield return (Statistic.SpecialDefense, SpecialDefense);
        yield return (Statistic.Speed, Speed);
        yield return (Statistic.Evasion, Evasion);
        yield return (Statistic.Accuracy, Accuracy);
    }

    /// <inheritdoc />
    public override sbyte GetStatistic(Statistic stat)
    {
        return stat switch
        {
            Statistic.Hp => Hp,
            Statistic.Attack => Attack,
            Statistic.Defense => Defense,
            Statistic.SpecialAttack => SpecialAttack,
            Statistic.SpecialDefense => SpecialDefense,
            Statistic.Speed => Speed,
            Statistic.Evasion => Evasion,
            Statistic.Accuracy => Accuracy,
            _ => throw new ArgumentException($"Invalid statistic {stat}"),
        };
    }

    /// <summary>
    /// Resets all statistics to 0.
    /// </summary>
    public void Reset()
    {
        Hp = 0;
        Attack = 0;
        Defense = 0;
        SpecialAttack = 0;
        SpecialDefense = 0;
        Speed = 0;
        Evasion = 0;
        Accuracy = 0;
    }
}

/// <summary>
/// A set of statistics that can be changed, but are clamped to a minimum and maximum value of 0 and 31.
/// </summary>
public record IndividualValueStatisticSet : ClampedStatisticSet<byte>
{
    /// <inheritdoc />
    protected override byte Min => 0;

    /// <inheritdoc />
    protected override byte Max => 31;

    /// <inheritdoc cref="IndividualValueStatisticSet"/>
    public IndividualValueStatisticSet() : base(0, 0, 0, 0, 0, 0)
    {
    }

    /// <inheritdoc cref="IndividualValueStatisticSet"/>
    public IndividualValueStatisticSet(byte hp, byte attack, byte defense, byte specialAttack, byte specialDefense,
        byte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
    {
    }

    /// <inheritdoc cref="IndividualValueStatisticSet"/>
    public IndividualValueStatisticSet(IndividualValueStatisticSet ivs) : base(ivs)
    {
    }
}

/// <summary>
/// A set of statistics that can be changed, but are clamped to a minimum and maximum value of 0 and 252.
/// </summary>
public record EffortValueStatisticSet : ClampedStatisticSet<byte>
{
    /// <inheritdoc />
    protected override byte Min => 0;

    /// <inheritdoc />
    protected override byte Max => 252;

    /// <inheritdoc cref="EffortValueStatisticSet"/>
    public EffortValueStatisticSet() : base(0, 0, 0, 0, 0, 0)
    {
    }

    /// <inheritdoc cref="EffortValueStatisticSet"/>
    public EffortValueStatisticSet(byte hp, byte attack, byte defense, byte specialAttack, byte specialDefense,
        byte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
    {
    }

    /// <inheritdoc cref="EffortValueStatisticSet"/>
    public EffortValueStatisticSet(EffortValueStatisticSet evs) : base(evs)
    {
    }
}