Code/General/FloatRange.cs
using Sandbox;
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ExtendedBox.General;

[JsonConverter(typeof(JsonConverter))]
public struct FloatRange : IEquatable<FloatRange>, IParsable<FloatRange> //, IFormattable // TODO: implement IFormattable when https://github.com/Facepunch/sbox-public/issues/10191 get resolved
{
    private float _min = 0;
    private float _max = 0;

    public float Min
    {
        readonly get => _min;
        set => _min = Math.Min(_max, value);
    }
    public float Max
    {
        readonly get => _max;
        set => _max = Math.Max(_min, value);
    }

    [JsonIgnore, Hide]
    public readonly float Delta => Max - Min;

    public FloatRange()
    {
    }

    [JsonConstructor]
    public FloatRange(float min, float max)
    {
        _min = Math.Min(min, max);
        _max = Math.Max(min, max);
    }

    public static FloatRange Parse(string? str, IFormatProvider? provider)
    {
        if(TryParse(str, provider, out var result))
            return result;

        return default;
    }

    public static FloatRange Parse(string? str)
    {
        return Parse(str, CultureInfo.InvariantCulture);
    }

    public static bool TryParse(string str, out FloatRange result)
    {
        return TryParse(str, CultureInfo.InvariantCulture, out result);
    }

    public static bool TryParse(string? str, IFormatProvider? provider, out FloatRange result)
    {
        result = default;
        if(string.IsNullOrWhiteSpace(str))
        {
            return false;
        }

        str = str.Trim('[', ']', ' ', '\n', '\r', '\t', '"');
        string[] array = str.Split([' ', ',', ';', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
        if(array.Length != 2)
        {
            return false;
        }

        if(!float.TryParse(array[0], NumberStyles.Float, provider, out var result2) ||
            !float.TryParse(array[1], NumberStyles.Float, provider, out var result3))
        {
            return false;
        }

        result = new(result2, result3);
        return true;
    }

    public readonly bool Equals(FloatRange other) => Min == other.Min && Max == other.Max;
    public override readonly bool Equals(object? obj) => obj is FloatRange other && Equals(other);
    public static bool operator ==(FloatRange left, FloatRange right) => left.Equals(right);
    public static bool operator !=(FloatRange left, FloatRange right) => !(left == right);

    public override readonly int GetHashCode() => HashCode.Combine(Min, Max);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public readonly string ToString(IFormatProvider? formatProvider) => ToString(null, formatProvider);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public readonly string ToString(string? format) => ToString(format, null);
    public readonly string ToString(string? format, IFormatProvider? formatProvider) => $"{Min.ToString(format, formatProvider)},{Max.ToString(format, formatProvider)}";
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public override readonly string ToString() => ToString(null, null);


    public class JsonConverter : JsonConverter<FloatRange>
    {
        public override FloatRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if(reader.TokenType == JsonTokenType.Null)
                return default;

            if(reader.TokenType == JsonTokenType.String)
                return Parse(reader.GetString());

            if(reader.TokenType == JsonTokenType.StartArray)
            {
                reader.Read();
                float min = 0;
                float max = 0;

                if(reader.TokenType == JsonTokenType.Number)
                {
                    max = min = reader.GetSingle();
                    reader.Read();
                }

                if(reader.TokenType == JsonTokenType.Number)
                {
                    max = reader.GetSingle();
                    reader.Read();
                }

                while(reader.TokenType != JsonTokenType.EndArray)
                {
                    reader.Read();
                }

                return new(min, max);
            }

            Log.Warning($"{typeof(JsonConverter).FullName} - unable to read from {reader.TokenType}");
            return default;
        }

        public override void Write(Utf8JsonWriter writer, FloatRange value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString());
        }

        public override FloatRange ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return Parse(reader.GetString());
        }

        public override void WriteAsPropertyName(Utf8JsonWriter writer, FloatRange value, JsonSerializerOptions options)
        {
            writer.WritePropertyName(value.ToString()!);
        }

        public override bool CanConvert(Type typeToConvert)
        {
            return typeToConvert == typeof(FloatRange) || typeToConvert == typeof(string);
        }
    }
}