Code/SmartEnum.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SmarterEnum;

public abstract class SmartEnum
{
    public string Name { get; }

    [SkipHotload]
    protected internal static readonly Dictionary<Type, Lazy<IReadOnlyList<SmartEnum>>> _allValues = []; // TODO remove and access values via better way


    protected internal SmartEnum(string name)
    {
        Name = name;
    }


    public static bool TryFromName(Type smartEnumType, string str, bool ignoreCase, out SmartEnum value)
    {
        if(!IsSmartEnum(smartEnumType))
            throw new ArgumentException($"Given type ({smartEnumType}) is not a ({nameof(SmartEnum)}).", nameof(smartEnumType));

        var values = GetValues(smartEnumType);

        foreach(var currentValue in values)
        {
            if(string.Equals(currentValue.Name, str, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
            {
                value = currentValue;
                return true;
            }
        }

        value = null!;
        return false;
    }

    internal protected static List<SmartEnum> FindAllValues(Type type)
    {
        return TypeLibrary.GetType(type).Fields
            .Where(x => x.FieldType.IsAssignableTo(type) && x.IsStatic && x.IsInitOnly)
            .Select(x => ((SmartEnum)x.GetValue(null)))
            .Order()
            .ToList();
    }

    public static IEnumerable<SmartEnum> GetValues(Type smartEnumType)
    {
        if(!IsSmartEnum(smartEnumType))
            throw new ArgumentException($"Given type ({smartEnumType}) is not a ({nameof(SmartEnum)}).", nameof(smartEnumType));

        if(!_allValues.ContainsKey(smartEnumType))
            _allValues[smartEnumType] = new(() => FindAllValues(smartEnumType));

        return _allValues[smartEnumType].Value;
    }

    public static bool IsSmartEnum(Type type)
    {
        return type.IsAssignableToGenericType(typeof(SmartEnum<,>));
    }
}

public class SmartEnum<TEnum> : SmartEnum<TEnum, int> where TEnum : SmartEnum<TEnum, int>
{
    protected SmartEnum(string name, int value) : base(name, value)
    {
    }
}

public class SmartEnum<TEnum, TValue> : SmartEnum, IComparable<SmartEnum<TEnum, TValue>>, IComparable, IEquatable<SmartEnum<TEnum, TValue>>
    where TEnum : SmartEnum<TEnum, TValue> where TValue : IComparable<TValue>, IEquatable<TValue>
{
    private static readonly Lazy<IReadOnlyList<TEnum>> _values = new(FindAllValues, true);
    private static readonly Lazy<Dictionary<string, TEnum>> _fromName = new(() => Values.ToDictionary(item => item.Name), true);
    private static readonly Lazy<Dictionary<string, TEnum>> _fromNameIgnoreCase = new(() => Values.ToDictionary(item => item.Name, StringComparer.OrdinalIgnoreCase), true);
    private static readonly Lazy<Dictionary<TValue, TEnum>> _fromValue =
            new(() =>
            {
                var dictionary = new Dictionary<TValue, TEnum>(GetValueComparer());
                foreach(var item in Values)
                {
                    if(item.Value is not null)
                        dictionary.TryAdd(item.Value, item);
                }
                return dictionary;
            }, true);

    public TValue Value { get; }

    public static IReadOnlyCollection<TEnum> Values => _values.Value;


    protected SmartEnum(string name, TValue value) : base(name)
    {
        Value = value;

        var type = GetType();
        if(!_allValues.ContainsKey(type))
            _allValues.Add(type, new(() => _values.Value));
    }

    private static List<TEnum> FindAllValues()
    {
        return SmartEnum.FindAllValues(typeof(TEnum)).Select(x => (TEnum)x).ToList();
    }

    public static bool IsDefined(TEnum value) => Values.Contains(value);

    public static TEnum FromName(string name, bool ignoreCase = false)
    {
        ArgumentNullException.ThrowIfNullOrEmpty(name);

        if(ignoreCase)
            return FromName(_fromNameIgnoreCase.Value);
        else
            return FromName(_fromName.Value);

        TEnum FromName(Dictionary<string, TEnum> dictionary)
        {
            if(!dictionary.TryGetValue(name, out var result))
                throw new ArgumentException($"Name {name} not found.", nameof(name));
            return result;
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool TryFromName(string name, out TEnum result) =>
            TryFromName(name, false, out result);

    public static bool TryFromName(string name, bool ignoreCase, out TEnum result)
    {
        if(String.IsNullOrEmpty(name))
        {
            result = default!;
            return false;
        }

        if(ignoreCase)
            return _fromNameIgnoreCase.Value.TryGetValue(name, out result!);
        else
            return _fromName.Value.TryGetValue(name, out result!);
    }

    public static TEnum FromValue(TValue value)
    {
        if(!TryFromValue(value, out var result))
            throw new ArgumentException($"Value {value} not found.", nameof(value));
        return result;
    }

    public static TEnum FromValue(TValue value, TEnum defaultValue)
    {
        ArgumentNullException.ThrowIfNull(value);

        if(!_fromValue.Value.TryGetValue(value, out var result))
            return defaultValue;
        return result;
    }

    public static bool TryFromValue(TValue value, out TEnum result)
    {
        if(value is not null)
        {
            if(!_fromValue.Value.TryGetValue(value, out result!))
                return false;
        }
        else
        {
            var nulls = _values.Value.Where(x => x.Value is null);
            result = nulls.FirstOrDefault()!;
            if(!nulls.Any())
                return false;
        }
        return true;
    }


    private static IEqualityComparer<TValue>? GetValueComparer()
    {
        var comparer = typeof(TEnum).GetCustomAttribute<SmartEnumComparerAttribute<TValue>>();
        return comparer?.Comparer;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool operator <(SmartEnum<TEnum, TValue> left, SmartEnum<TEnum, TValue> right) =>
            left.CompareTo(right) < 0;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool operator <=(SmartEnum<TEnum, TValue> left, SmartEnum<TEnum, TValue> right) =>
            left.CompareTo(right) <= 0;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool operator >(SmartEnum<TEnum, TValue> left, SmartEnum<TEnum, TValue> right) =>
            left.CompareTo(right) > 0;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool operator >=(SmartEnum<TEnum, TValue> left, SmartEnum<TEnum, TValue> right) =>
            left.CompareTo(right) >= 0;

    public static bool operator ==(SmartEnum<TEnum, TValue> left, SmartEnum<TEnum, TValue> right)
    {
        if(left is null)
            return right is null;

        return left.Equals(right);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool operator !=(SmartEnum<TEnum, TValue> left, SmartEnum<TEnum, TValue> right) => !(left == right);


    public int CompareTo(SmartEnum<TEnum, TValue>? other)
    {
        if(other is null)
            return 1;

        return Value.CompareTo(other.Value);
    }

    public int CompareTo(object? obj)
    {
        if(obj is null)
            return 1;

        if(obj is not SmartEnum<TEnum, TValue> other)
            throw new ArgumentException($"Object is not a {typeof(SmartEnum<TEnum, TValue>)}.", nameof(obj));

        return CompareTo(other);
    }

    public bool Equals(SmartEnum<TEnum, TValue>? other)
    {
        if(object.ReferenceEquals(this, other))
            return true;

        if(other is null)
            return false;

        return Value.Equals(other.Value);
    }

    public override bool Equals(object? obj) => obj is SmartEnum<TEnum, TValue> other && Equals(other);


    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static explicit operator SmartEnum<TEnum, TValue>(TValue value) =>
            FromValue(value);


    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public override int GetHashCode() => Value.GetHashCode();

    public override string ToString() => Name;


    public class SmartEnumJsonConverter : JsonConverter<TEnum>
    {
        protected virtual string ValueToString(TEnum value) => value.ToString();
        protected virtual bool TryParse(string str, out TEnum value) => TryFromName(str, out value);


        public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
                    Read(ref reader, typeToConvert, JsonTokenType.String);

        public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) =>
            writer.WriteStringValue(ValueToString(value));

        public override TEnum ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
            Read(ref reader, typeToConvert, JsonTokenType.PropertyName);

        public override void WriteAsPropertyName(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) =>
            writer.WritePropertyName(ValueToString(value));


        protected virtual TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonTokenType validTokenType)
        {
            if(reader.TokenType == validTokenType)
            {
                var str = reader.GetString();
                if(!string.IsNullOrWhiteSpace(str) && TryParse(str, out var value))
                    return value;
            }

            Log.Warning($"{GetType().Name} - unable to read from {reader.TokenType}");
            return default!;
        }
    }
}