using PkmnLib.Static.Utils;

namespace PkmnLib.Static.Libraries;

/// <summary>
/// All data related to types and their effectiveness.
/// </summary>
public interface IReadOnlyTypeLibrary
{
    /// <summary>
    /// Gets the type identifier for a type with a name.
    /// </summary>
    bool TryGetTypeIdentifier(StringKey key, out TypeIdentifier typeIdentifier);

    /// <summary>
    /// Gets the type identifier for a type with an index.
    /// </summary>
    bool TryGetTypeIdentifierFromIndex(byte index, out TypeIdentifier typeIdentifier);

    /// <summary>
    /// Gets the effectiveness for a single attacking type against a single defending type.
    /// </summary>
    float GetSingleEffectiveness(TypeIdentifier attacking, TypeIdentifier defending);

    /// <summary>
    /// Gets the effectiveness for a single attacking type against an amount of defending types.
    /// This is equivalent to running get_single_effectiveness on each defending type, and multiplying the results with each other.
    /// </summary>
    float GetEffectiveness(TypeIdentifier attacking, IEnumerable<TypeIdentifier> defending);

    /// <summary>
    /// Gets the effectiveness for a single attacking type against all defending types.
    /// </summary>
    IEnumerable<(TypeIdentifier type, float effectiveness)> GetAllEffectivenessFromAttacking(TypeIdentifier attacking);
}

/// <inheritdoc />
public class TypeLibrary : IReadOnlyTypeLibrary
{
    private readonly List<TypeIdentifier> _types = new();
    private readonly List<List<float>> _effectiveness = new();

    /// <inheritdoc />
    public bool TryGetTypeIdentifier(StringKey key, out TypeIdentifier type)
    {
        var found = _types.FirstOrDefault(t => t.Name.Equals(key));
        if (found.Value is not 0)
        {
            type = found;
            return true;
        }
        type = default;
        return false;
    }

    /// <inheritdoc />
    public bool TryGetTypeIdentifierFromIndex(byte index, out TypeIdentifier typeIdentifier)
    {
        if (index < 1 || index > _types.Count)
        {
            typeIdentifier = default;
            return false;
        }
        typeIdentifier = _types[index - 1];
        return true;
    }

    /// <inheritdoc />
    public float GetSingleEffectiveness(TypeIdentifier attacking, TypeIdentifier defending)
    {
        if (attacking.Value < 1 || attacking.Value > _effectiveness.Count)
            return 1;
        if (defending.Value < 1 || defending.Value > _effectiveness.Count)
            return 1;
        return _effectiveness[attacking.Value - 1][defending.Value - 1];
    }

    /// <inheritdoc />
    public float GetEffectiveness(TypeIdentifier attacking, IEnumerable<TypeIdentifier> defending) =>
        defending.Aggregate<TypeIdentifier, float>(1,
            (current, type) => current * GetSingleEffectiveness(attacking, type));

    /// <inheritdoc />
    public IEnumerable<(TypeIdentifier, float)> GetAllEffectivenessFromAttacking(TypeIdentifier attacking)
    {
        if (attacking.Value < 1 || attacking.Value > _effectiveness.Count)
        {
            throw new ArgumentOutOfRangeException(nameof(attacking), attacking,
                "Attacking type index is out of range.");
        }
        for (var i = 0; i < _effectiveness.Count; i++)
        {
            var type = _types[i];
            yield return (type, _effectiveness[attacking.Value - 1][i]);
        }
    }

    /// <summary>
    /// Registers a new type in the library.
    /// </summary>
    public TypeIdentifier RegisterType(StringKey name)
    {
        var id = new TypeIdentifier((byte)(_types.Count + 1), name);
        _types.Add(id);
        _effectiveness.Add(Enumerable.Repeat(1.0f, _effectiveness.Count).ToList());
        foreach (var list in _effectiveness)
        {
            list.Add(1);
        }
        return id;
    }

    /// <summary>
    /// Sets the effectiveness for an attacking type against a defending type.
    /// </summary>
    public void SetEffectiveness(TypeIdentifier attacking, TypeIdentifier defending, float effectiveness)
    {
        if (attacking.Value < 1 || attacking.Value > _effectiveness.Count)
            throw new ArgumentOutOfRangeException(nameof(attacking));
        if (defending.Value < 1 || defending.Value > _effectiveness.Count)
            throw new ArgumentOutOfRangeException(nameof(defending));
        _effectiveness[attacking.Value - 1][defending.Value - 1] = effectiveness;
    }
}