using System.Text.Json;
using System.Text.Json.Serialization;

namespace FrameProcessor.Domain;

/// <summary>
/// URL-friendly identifier for a frame. Valid characters are the RFC 3986
/// "unreserved" set: <c>A-Z a-z 0-9 - . _ ~</c>. Must be non-empty.
/// </summary>
[JsonConverter(typeof(FrameNameJsonConverter))]
public readonly record struct FrameName : IParsable<FrameName>, ISpanParsable<FrameName>
{
    private readonly string? _value;

    private FrameName(string value)
    {
        _value = value;
    }

    public string Value => _value ?? string.Empty;

    public override string ToString() => Value;

    public static bool TryParse(string? s, out FrameName result)
    {
        if (s is null)
        {
            result = default;
            return false;
        }

        return TryParseCore(s.AsSpan(), s, out result);
    }

    public static bool TryParse(ReadOnlySpan<char> s, out FrameName result)
        => TryParseCore(s, null, out result);

    private static bool TryParseCore(ReadOnlySpan<char> s, string? original, out FrameName result)
    {
        result = default;
        if (s.IsEmpty)
        {
            return false;
        }

        foreach (var c in s)
        {
            if (!IsUnreserved(c))
            {
                return false;
            }
        }

        result = new FrameName(original ?? new string(s));
        return true;
    }

    public static FrameName Parse(string s)
    {
        ArgumentNullException.ThrowIfNull(s);
        return TryParse(s, out var result)
            ? result
            : throw new FormatException(
                $"'{s}' is not a valid frame name. Allowed characters: A-Z a-z 0-9 - . _ ~ (non-empty).");
    }

    public static FrameName Parse(string s, IFormatProvider? provider) => Parse(s);

    public static bool TryParse(string? s, IFormatProvider? provider, out FrameName result)
        => TryParse(s, out result);

    public static FrameName Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
        => TryParse(s, out var result)
            ? result
            : throw new FormatException(
                $"'{new string(s)}' is not a valid frame name. Allowed characters: A-Z a-z 0-9 - . _ ~ (non-empty).");

    public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out FrameName result)
        => TryParse(s, out result);

    private static bool IsUnreserved(char c)
        => (uint)(c - 'A') <= 25
            || (uint)(c - 'a') <= 25
            || (uint)(c - '0') <= 9
            || c is '-' or '.' or '_' or '~';
}

internal sealed class FrameNameJsonConverter : JsonConverter<FrameName>
{
    public override FrameName Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var s = reader.GetString();
        if (!FrameName.TryParse(s, out var name))
        {
            throw new JsonException(
                $"'{s}' is not a valid frame name. Allowed characters: A-Z a-z 0-9 - . _ ~ (non-empty).");
        }

        return name;
    }

    public override void Write(Utf8JsonWriter writer, FrameName value, JsonSerializerOptions options)
        => writer.WriteStringValue(value.Value);
}
