// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.Json;

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Performs conversions during binding.
/// </summary>
//
// Perf: our conversion routines present a regular API surface that allows us to specialize on types to avoid boxing.
// for instance, many of these types could be cast to IFormattable to do the appropriate formatting, but that's going
// to allocate.
public static class BindConverter
{
    private static readonly object BoxedTrue = true;
    private static readonly object BoxedFalse = false;

    private delegate object? BindFormatter<T>(T value, CultureInfo? culture);

    internal delegate bool BindParser<T>(object? obj, CultureInfo? culture, [MaybeNullWhen(false)] out T value);
    internal delegate bool BindParserWithFormat<T>(object? obj, CultureInfo? culture, string? format, [MaybeNullWhen(false)] out T value);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(string? value, CultureInfo? culture = null) => FormatStringValueCore(value, culture);

    private static string? FormatStringValueCore(string? value, CultureInfo? _)
    {
        return value;
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static bool FormatValue(bool value, CultureInfo? culture = null)
    {
        // Formatting for bool is special-cased. We need to produce a boolean value for conditional attributes
        // to work.
        return value;
    }

    // Used with generics
    private static object FormatBoolValueCore(bool value, CultureInfo? _)
    {
        // Formatting for bool is special-cased. We need to produce a boolean value for conditional attributes
        // to work.
        return value ? BoxedTrue : BoxedFalse;
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static bool? FormatValue(bool? value, CultureInfo? culture = null)
    {
        // Formatting for bool is special-cased. We need to produce a boolean value for conditional attributes
        // to work.
        return value == null ? (bool?)null : value.Value;
    }

    // Used with generics
    private static object? FormatNullableBoolValueCore(bool? value, CultureInfo? _)
    {
        // Formatting for bool is special-cased. We need to produce a boolean value for conditional attributes
        // to work.
        return value == null ? null : value.Value ? BoxedTrue : BoxedFalse;
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(int value, CultureInfo? culture = null) => FormatIntValueCore(value, culture);

    private static string? FormatIntValueCore(int value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(int? value, CultureInfo? culture = null) => FormatNullableIntValueCore(value, culture);

    private static string? FormatNullableIntValueCore(int? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(long value, CultureInfo? culture = null) => FormatLongValueCore(value, culture);

    private static string FormatLongValueCore(long value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(long? value, CultureInfo? culture = null) => FormatNullableLongValueCore(value, culture);

    private static string? FormatNullableLongValueCore(long? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(short value, CultureInfo? culture = null) => FormatShortValueCore(value, culture);

    private static string FormatShortValueCore(short value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(short? value, CultureInfo? culture = null) => FormatNullableShortValueCore(value, culture);

    private static string? FormatNullableShortValueCore(short? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(float value, CultureInfo? culture = null) => FormatFloatValueCore(value, culture);

    private static string FormatFloatValueCore(float value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(float? value, CultureInfo? culture = null) => FormatNullableFloatValueCore(value, culture);

    private static string? FormatNullableFloatValueCore(float? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(double value, CultureInfo? culture = null) => FormatDoubleValueCore(value, culture);

    private static string FormatDoubleValueCore(double value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(double? value, CultureInfo? culture = null) => FormatNullableDoubleValueCore(value, culture);

    private static string? FormatNullableDoubleValueCore(double? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> for inclusion in an attribute.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(decimal value, CultureInfo? culture = null) => FormatDecimalValueCore(value, culture);

    private static string FormatDecimalValueCore(decimal value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(decimal? value, CultureInfo? culture = null) => FormatNullableDecimalValueCore(value, culture);

    private static string? FormatNullableDecimalValueCore(decimal? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(DateTime value, CultureInfo? culture = null) => FormatDateTimeValueCore(value, format: null, culture);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="format">The format to use. Provided to <see cref="DateTimeOffset.ToString(string, IFormatProvider)"/>.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(DateTime value, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string format, CultureInfo? culture = null) => FormatDateTimeValueCore(value, format, culture);

    private static string FormatDateTimeValueCore(DateTime value, string? format, CultureInfo? culture)
    {
        if (format != null)
        {
            return value.ToString(format, culture ?? CultureInfo.CurrentCulture);
        }

        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string FormatDateTimeValueCore(DateTime value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(DateTime? value, CultureInfo? culture = null) => FormatNullableDateTimeValueCore(value, format: null, culture);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="format">The format to use. Provided to <see cref="DateTime.ToString(string, IFormatProvider)"/>.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(DateTime? value, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string? format, CultureInfo? culture = null) => FormatNullableDateTimeValueCore(value, format, culture);

    private static string? FormatNullableDateTimeValueCore(DateTime? value, string? format, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        if (format != null)
        {
            return value.Value.ToString(format, culture ?? CultureInfo.CurrentCulture);
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string? FormatNullableDateTimeValueCore(DateTime? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(DateTimeOffset value, CultureInfo? culture = null) => FormatDateTimeOffsetValueCore(value, format: null, culture);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="format">The format to use. Provided to <see cref="DateTimeOffset.ToString(string, IFormatProvider)"/>.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(DateTimeOffset value, string format, CultureInfo? culture = null) => FormatDateTimeOffsetValueCore(value, format, culture);

    private static string FormatDateTimeOffsetValueCore(DateTimeOffset value, string? format, CultureInfo? culture)
    {
        if (format != null)
        {
            return value.ToString(format, culture ?? CultureInfo.CurrentCulture);
        }

        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string FormatDateTimeOffsetValueCore(DateTimeOffset value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(DateTimeOffset? value, CultureInfo? culture = null) => FormatNullableDateTimeOffsetValueCore(value, format: null, culture);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="format">The format to use. Provided to <see cref="DateTimeOffset.ToString(string, IFormatProvider)"/>.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(DateTimeOffset? value, string format, CultureInfo? culture = null) => FormatNullableDateTimeOffsetValueCore(value, format, culture);

    private static string? FormatNullableDateTimeOffsetValueCore(DateTimeOffset? value, string? format, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        if (format != null)
        {
            return value.Value.ToString(format, culture ?? CultureInfo.CurrentCulture);
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string? FormatNullableDateTimeOffsetValueCore(DateTimeOffset? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(DateOnly value, CultureInfo? culture = null) => FormatDateOnlyValueCore(value, format: null, culture);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="format">The format to use. Provided to <see cref="DateOnly.ToString(string, IFormatProvider)"/>.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(DateOnly value, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] string format, CultureInfo? culture = null) => FormatDateOnlyValueCore(value, format, culture);

    private static string FormatDateOnlyValueCore(DateOnly value, string? format, CultureInfo? culture)
    {
        if (format != null)
        {
            // We convert to a DateTime so formatting doesn't throw if the format includes time information
            return value.ToDateTime(TimeOnly.MinValue).ToString(format, culture ?? CultureInfo.CurrentCulture);
        }

        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string FormatDateOnlyValueCore(DateOnly value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(DateOnly? value, CultureInfo? culture = null) => FormatNullableDateOnlyValueCore(value, format: null, culture);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="format">The format to use. Provided to <see cref="DateOnly.ToString(string, IFormatProvider)"/>.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(DateOnly? value, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] string format, CultureInfo? culture = null) => FormatNullableDateOnlyValueCore(value, format, culture);

    private static string? FormatNullableDateOnlyValueCore(DateOnly? value, string? format, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        if (format != null)
        {
            // We convert to a DateTime so formatting doesn't throw if the format includes time information
            return value.Value.ToDateTime(TimeOnly.MinValue).ToString(format, culture ?? CultureInfo.CurrentCulture);
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string? FormatNullableDateOnlyValueCore(DateOnly? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(TimeOnly value, CultureInfo? culture = null) => FormatTimeOnlyValueCore(value, format: null, culture);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="format">The format to use. Provided to <see cref="DateOnly.ToString(string, IFormatProvider)"/>.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string FormatValue(TimeOnly value, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] string format, CultureInfo? culture = null) => FormatTimeOnlyValueCore(value, format, culture);

    private static string FormatTimeOnlyValueCore(TimeOnly value, string? format, CultureInfo? culture)
    {
        if (format != null)
        {
            // We convert to a DateTime so formatting doesn't throw if the format includes date information
            return DateTime.MinValue.Add(value.ToTimeSpan()).ToString(format, culture ?? CultureInfo.CurrentCulture);
        }

        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string FormatTimeOnlyValueCore(TimeOnly value, CultureInfo? culture)
    {
        return value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(TimeOnly? value, CultureInfo? culture = null) => FormatNullableTimeOnlyValueCore(value, format: null, culture);

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="format">The format to use. Provided to <see cref="DateOnly.ToString(string, IFormatProvider)"/>.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static string? FormatValue(TimeOnly? value, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] string format, CultureInfo? culture = null) => FormatNullableTimeOnlyValueCore(value, format, culture);

    private static string? FormatNullableTimeOnlyValueCore(TimeOnly? value, string? format, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        if (format != null)
        {
            // We convert to a DateTime so formatting doesn't throw if the format includes date information
            return DateTime.MinValue.Add(value.Value.ToTimeSpan()).ToString(format, culture ?? CultureInfo.CurrentCulture);
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string? FormatNullableTimeOnlyValueCore(TimeOnly? value, CultureInfo? culture)
    {
        if (value == null)
        {
            return null;
        }

        return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
    }

    private static string? FormatEnumValueCore<T>(T value, CultureInfo? _)
    {
        if (value == null)
        {
            return null;
        }

        return value.ToString();
    }

    /// <summary>
    /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
    /// </summary>
    /// <param name="value">The value to format.</param>
    /// <param name="culture">
    /// The <see cref="CultureInfo"/> to use while formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.
    /// </param>
    /// <returns>The formatted value.</returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static object? FormatValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T value, CultureInfo? culture = null)
    {
        var formatter = FormatterDelegateCache.Get<T>();
        return formatter(value, culture);
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.String"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToString(object? obj, CultureInfo? culture, out string? value)
    {
        return ConvertToStringCore(obj, culture, out value);
    }

    internal static readonly BindParser<string?> ConvertToString = ConvertToStringCore;

    private static bool ConvertToStringCore(object? obj, CultureInfo? culture, out string? value)
    {
        // We expect the input to already be a string.
        value = (string?)obj;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.Boolean"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToBool(object? obj, CultureInfo? culture, out bool value)
    {
        return ConvertToBoolCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.Boolean"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableBool(object? obj, CultureInfo? culture, out bool? value)
    {
        return ConvertToNullableBoolCore(obj, culture, out value);
    }

    internal static readonly BindParser<bool> ConvertToBool = ConvertToBoolCore;
    internal static readonly BindParser<bool?> ConvertToNullableBool = ConvertToNullableBoolCore;

    private static bool ConvertToBoolCore(object? obj, CultureInfo? culture, out bool value)
    {
        // We expect the input to already be a bool.
        value = (bool)obj!;
        return true;
    }

    private static bool ConvertToNullableBoolCore(object? obj, CultureInfo? culture, out bool? value)
    {
        // We expect the input to already be a bool.
        value = (bool?)obj;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.Int32"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToInt(object? obj, CultureInfo? culture, out int value)
    {
        return ConvertToIntCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.Int32"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableInt(object? obj, CultureInfo? culture, out int? value)
    {
        return ConvertToNullableIntCore(obj, culture, out value);
    }

    internal static BindParser<int> ConvertToInt = ConvertToIntCore;
    internal static BindParser<int?> ConvertToNullableInt = ConvertToNullableIntCore;

    private static bool ConvertToIntCore(object? obj, CultureInfo? culture, out int value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return false;
        }

        if (!int.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    private static bool ConvertToNullableIntCore(object? obj, CultureInfo? culture, out int? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!int.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.Int64"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToLong(object? obj, CultureInfo? culture, out long value)
    {
        return ConvertToLongCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.Int64"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableLong(object? obj, CultureInfo? culture, out long? value)
    {
        return ConvertToNullableLongCore(obj, culture, out value);
    }

    internal static BindParser<long> ConvertToLong = ConvertToLongCore;
    internal static BindParser<long?> ConvertToNullableLong = ConvertToNullableLongCore;

    private static bool ConvertToLongCore(object? obj, CultureInfo? culture, out long value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return false;
        }

        if (!long.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    private static bool ConvertToNullableLongCore(object? obj, CultureInfo? culture, out long? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!long.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.Int16"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToShort(object? obj, CultureInfo? culture, out short value)
    {
        return ConvertToShortCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.Int16"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableShort(object? obj, CultureInfo? culture, out short? value)
    {
        return ConvertToNullableShort(obj, culture, out value);
    }

    internal static BindParser<short> ConvertToShort = ConvertToShortCore;
    internal static BindParser<short?> ConvertToNullableShort = ConvertToNullableShortCore;

    private static bool ConvertToShortCore(object? obj, CultureInfo? culture, out short value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return false;
        }

        if (!short.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    private static bool ConvertToNullableShortCore(object? obj, CultureInfo? culture, out short? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!short.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.Single"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToFloat(object? obj, CultureInfo? culture, out float value)
    {
        return ConvertToFloatCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.Single"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableFloat(object? obj, CultureInfo? culture, out float? value)
    {
        return ConvertToNullableFloatCore(obj, culture, out value);
    }

    internal static BindParser<float> ConvertToFloat = ConvertToFloatCore;
    internal static BindParser<float?> ConvertToNullableFloat = ConvertToNullableFloatCore;

    private static bool ConvertToFloatCore(object? obj, CultureInfo? culture, out float value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return false;
        }

        if (!float.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        if (float.IsInfinity(converted) || float.IsNaN(converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    private static bool ConvertToNullableFloatCore(object? obj, CultureInfo? culture, out float? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!float.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        if (float.IsInfinity(converted) || float.IsNaN(converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.Double"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToDouble(object? obj, CultureInfo? culture, out double value)
    {
        return ConvertToDoubleCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.Double"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableDouble(object? obj, CultureInfo? culture, out double? value)
    {
        return ConvertToNullableDoubleCore(obj, culture, out value);
    }

    internal static BindParser<double> ConvertToDoubleDelegate = ConvertToDoubleCore;
    internal static BindParser<double?> ConvertToNullableDoubleDelegate = ConvertToNullableDoubleCore;

    private static bool ConvertToDoubleCore(object? obj, CultureInfo? culture, out double value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return false;
        }

        if (!double.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        if (double.IsInfinity(converted) || double.IsNaN(converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    private static bool ConvertToNullableDoubleCore(object? obj, CultureInfo? culture, out double? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!double.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        if (double.IsInfinity(converted) || double.IsNaN(converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.Decimal"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToDecimal(object? obj, CultureInfo? culture, out decimal value)
    {
        return ConvertToDecimalCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.Decimal"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableDecimal(object? obj, CultureInfo? culture, out decimal? value)
    {
        return ConvertToNullableDecimalCore(obj, culture, out value);
    }

    internal static BindParser<decimal> ConvertToDecimal = ConvertToDecimalCore;
    internal static BindParser<decimal?> ConvertToNullableDecimal = ConvertToNullableDecimalCore;

    private static bool ConvertToDecimalCore(object? obj, CultureInfo? culture, out decimal value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return false;
        }

        if (!decimal.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    private static bool ConvertToNullableDecimalCore(object? obj, CultureInfo? culture, out decimal? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!decimal.TryParse(text, NumberStyles.Number, culture ?? CultureInfo.CurrentCulture, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.DateTime"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToDateTime(object? obj, CultureInfo? culture, out DateTime value)
    {
        return ConvertToDateTimeCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.DateTime"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="format">The format string to use in conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToDateTime(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string format, out DateTime value)
    {
        return ConvertToDateTimeCore(obj, culture, format, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.DateTime"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableDateTime(object? obj, CultureInfo? culture, out DateTime? value)
    {
        return ConvertToNullableDateTimeCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.DateTime"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="format">The format string to use in conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableDateTime(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string format, out DateTime? value)
    {
        return ConvertToNullableDateTimeCore(obj, culture, format, out value);
    }

    internal static BindParser<DateTime> ConvertToDateTime = ConvertToDateTimeCore;
    internal static BindParserWithFormat<DateTime> ConvertToDateTimeWithFormat = ConvertToDateTimeCore;
    internal static BindParser<DateTime?> ConvertToNullableDateTime = ConvertToNullableDateTimeCore;
    internal static BindParserWithFormat<DateTime?> ConvertToNullableDateTimeWithFormat = ConvertToNullableDateTimeCore;

    private static bool ConvertToDateTimeCore(object? obj, CultureInfo? culture, out DateTime value)
    {
        return ConvertToDateTimeCore(obj, culture, format: null, out value);
    }

    private static bool ConvertToDateTimeCore(object? obj, CultureInfo? culture, string? format, out DateTime value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return false;
        }

        if (format != null && DateTime.TryParseExact(text, format, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
        {
            value = converted;
            return true;
        }
        else if (format == null && DateTime.TryParse(text, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out converted))
        {
            value = converted;
            return true;
        }

        value = default;
        return false;
    }

    private static bool ConvertToNullableDateTimeCore(object? obj, CultureInfo? culture, out DateTime? value)
    {
        return ConvertToNullableDateTimeCore(obj, culture, format: null, out value);
    }

    private static bool ConvertToNullableDateTimeCore(object? obj, CultureInfo? culture, string? format, out DateTime? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (format != null && DateTime.TryParseExact(text, format, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
        {
            value = converted;
            return true;
        }
        else if (format == null && DateTime.TryParse(text, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out converted))
        {
            value = converted;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.DateTimeOffset"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToDateTimeOffset(object? obj, CultureInfo? culture, out DateTimeOffset value)
    {
        return ConvertToDateTimeOffsetCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.DateTimeOffset"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="format">The format string to use in conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToDateTimeOffset(object? obj, CultureInfo? culture, string format, out DateTimeOffset value)
    {
        return ConvertToDateTimeOffsetCore(obj, culture, format, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.DateTimeOffset"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableDateTimeOffset(object? obj, CultureInfo? culture, out DateTimeOffset? value)
    {
        return ConvertToNullableDateTimeOffsetCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.DateTimeOffset"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="format">The format string to use in conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableDateTimeOffset(object? obj, CultureInfo? culture, string format, out DateTimeOffset? value)
    {
        return ConvertToNullableDateTimeOffsetCore(obj, culture, format, out value);
    }

    internal static BindParser<DateTimeOffset> ConvertToDateTimeOffset = ConvertToDateTimeOffsetCore;
    internal static BindParserWithFormat<DateTimeOffset> ConvertToDateTimeOffsetWithFormat = ConvertToDateTimeOffsetCore;
    internal static BindParser<DateTimeOffset?> ConvertToNullableDateTimeOffset = ConvertToNullableDateTimeOffsetCore;
    internal static BindParserWithFormat<DateTimeOffset?> ConvertToNullableDateTimeOffsetWithFormat = ConvertToNullableDateTimeOffsetCore;

    private static bool ConvertToDateTimeOffsetCore(object? obj, CultureInfo? culture, out DateTimeOffset value)
    {
        return ConvertToDateTimeOffsetCore(obj, culture, format: null, out value);
    }

    private static bool ConvertToDateTimeOffsetCore(object? obj, CultureInfo? culture, string? format, out DateTimeOffset value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return false;
        }

        if (format != null && DateTimeOffset.TryParseExact(text, format, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
        {
            value = converted;
            return true;
        }
        else if (format == null && DateTimeOffset.TryParse(text, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out converted))
        {
            value = converted;
            return true;
        }

        value = default;
        return false;
    }

    private static bool ConvertToNullableDateTimeOffsetCore(object? obj, CultureInfo? culture, out DateTimeOffset? value)
    {
        return ConvertToNullableDateTimeOffsetCore(obj, culture, format: null, out value);
    }

    private static bool ConvertToNullableDateTimeOffsetCore(object? obj, CultureInfo? culture, string? format, out DateTimeOffset? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (format != null && DateTimeOffset.TryParseExact(text, format, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out var converted))
        {
            value = converted;
            return true;
        }
        else if (format == null && DateTimeOffset.TryParse(text, culture ?? CultureInfo.CurrentCulture, DateTimeStyles.None, out converted))
        {
            value = converted;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.DateOnly"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToDateOnly(object? obj, CultureInfo? culture, out DateOnly value)
    {
        return ConvertToDateOnlyCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.DateOnly"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="format">The format string to use in conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToDateOnly(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] string format, out DateOnly value)
    {
        return ConvertToDateOnlyCore(obj, culture, format, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.DateOnly"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableDateOnly(object? obj, CultureInfo? culture, out DateOnly? value)
    {
        return ConvertToNullableDateOnlyCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.DateOnly"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="format">The format string to use in conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableDateOnly(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] string format, out DateOnly? value)
    {
        return ConvertToNullableDateOnlyCore(obj, culture, format, out value);
    }

    internal static BindParser<DateOnly> ConvertToDateOnly = ConvertToDateOnlyCore;
    internal static BindParserWithFormat<DateOnly> ConvertToDateOnlyWithFormat = ConvertToDateOnlyCore;
    internal static BindParser<DateOnly?> ConvertToNullableDateOnly = ConvertToNullableDateOnlyCore;
    internal static BindParserWithFormat<DateOnly?> ConvertToNullableDateOnlyWithFormat = ConvertToNullableDateOnlyCore;

    private static bool ConvertToDateOnlyCore(object? obj, CultureInfo? culture, out DateOnly value)
    {
        return ConvertToDateOnlyCore(obj, culture, format: null, out value);
    }

    private static bool ConvertToDateOnlyCore(object? obj, CultureInfo? culture, string? format, out DateOnly value)
    {
        // We first convert to a DateTime so conversion doesn't fail if time information is included
        if (ConvertToDateTimeCore(obj, culture, format, out var dateTime))
        {
            value = DateOnly.FromDateTime(dateTime);
            return true;
        }

        value = default;
        return false;
    }

    private static bool ConvertToNullableDateOnlyCore(object? obj, CultureInfo? culture, out DateOnly? value)
    {
        return ConvertToNullableDateOnlyCore(obj, culture, format: null, out value);
    }

    private static bool ConvertToNullableDateOnlyCore(object? obj, CultureInfo? culture, string? format, out DateOnly? value)
    {
        // We first convert to a DateTime so conversion doesn't fail if time information is included
        if (ConvertToDateTimeCore(obj, culture, format, out var dateTime))
        {
            value = DateOnly.FromDateTime(dateTime);
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.TimeOnly"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToTimeOnly(object? obj, CultureInfo? culture, out TimeOnly value)
    {
        return ConvertToTimeOnlyCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a <see cref="System.TimeOnly"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="format">The format string to use in conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToTimeOnly(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] string format, out TimeOnly value)
    {
        return ConvertToTimeOnlyCore(obj, culture, format, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.TimeOnly"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableTimeOnly(object? obj, CultureInfo? culture, out TimeOnly? value)
    {
        return ConvertToNullableTimeOnlyCore(obj, culture, out value);
    }

    /// <summary>
    /// Attempts to convert a value to a nullable <see cref="System.TimeOnly"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="format">The format string to use in conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertToNullableTimeOnly(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] string format, out TimeOnly? value)
    {
        return ConvertToNullableTimeOnlyCore(obj, culture, format, out value);
    }

    internal static BindParser<TimeOnly> ConvertToTimeOnly = ConvertToTimeOnlyCore;
    internal static BindParserWithFormat<TimeOnly> ConvertToTimeOnlyWithFormat = ConvertToTimeOnlyCore;
    internal static BindParser<TimeOnly?> ConvertToNullableTimeOnly = ConvertToNullableTimeOnlyCore;
    internal static BindParserWithFormat<TimeOnly?> ConvertToNullableTimeOnlyWithFormat = ConvertToNullableTimeOnlyCore;

    private static bool ConvertToTimeOnlyCore(object? obj, CultureInfo? culture, out TimeOnly value)
    {
        return ConvertToTimeOnlyCore(obj, culture, format: null, out value);
    }

    private static bool ConvertToTimeOnlyCore(object? obj, CultureInfo? culture, string? format, out TimeOnly value)
    {
        // We first convert to a DateTime so conversion doesn't fail if time information is included
        if (ConvertToDateTimeCore(obj, culture, format, out var dateTime))
        {
            value = TimeOnly.FromDateTime(dateTime);
            return true;
        }

        value = default;
        return false;
    }

    private static bool ConvertToNullableTimeOnlyCore(object? obj, CultureInfo? culture, out TimeOnly? value)
    {
        return ConvertToNullableTimeOnlyCore(obj, culture, format: null, out value);
    }

    private static bool ConvertToNullableTimeOnlyCore(object? obj, CultureInfo? culture, string? format, out TimeOnly? value)
    {
        // We first convert to a DateTime so conversion doesn't fail if time information is included
        if (ConvertToDateTimeCore(obj, culture, format, out var dateTime))
        {
            value = TimeOnly.FromDateTime(dateTime);
            return true;
        }

        value = default;
        return false;
    }

    internal static readonly BindParser<Guid> ConvertToGuid = ConvertToGuidCore;
    internal static readonly BindParser<Guid?> ConvertToNullableGuid = ConvertToNullableGuidCore;

    private static bool ConvertToGuidCore(object? obj, CultureInfo? culture, out Guid value)
    {
        ConvertToNullableGuidCore(obj, culture, out var converted);
        value = converted.GetValueOrDefault();
        return converted.HasValue;
    }

    private static bool ConvertToNullableGuidCore(object? obj, CultureInfo? culture, out Guid? value)
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!Guid.TryParse(text, out var converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    private static bool ConvertToEnum<T>(object? obj, CultureInfo? _, out T value) where T : struct, Enum
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!Enum.TryParse<T>(text, out var converted))
        {
            value = default;
            return false;
        }

        if (!Enum.IsDefined(typeof(T), converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    private static bool ConvertToNullableEnum<T>(object? obj, CultureInfo? _, out T? value) where T : struct, Enum
    {
        var text = (string?)obj;
        if (string.IsNullOrEmpty(text))
        {
            value = default;
            return true;
        }

        if (!Enum.TryParse<T>(text, out var converted))
        {
            value = default;
            return false;
        }

        if (!Enum.IsDefined(typeof(T), converted))
        {
            value = default;
            return false;
        }

        value = converted;
        return true;
    }

    /// <summary>
    /// Attempts to convert a value to a value of type <typeparamref name="T"/>.
    /// </summary>
    /// <param name="obj">The object to convert.</param>
    /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
    /// <param name="value">The converted value.</param>
    /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
    public static bool TryConvertTo<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(object? obj, CultureInfo? culture, [MaybeNullWhen(false)] out T value)
    {
        var converter = ParserDelegateCache.Get<T>();
        return converter(obj, culture, out value);
    }

    private static class FormatterDelegateCache
    {
        private static readonly ConcurrentDictionary<Type, Delegate> _cache = new ConcurrentDictionary<Type, Delegate>();

        private static MethodInfo? _makeArrayFormatter;

        [UnconditionalSuppressMessage(
            "ReflectionAnalysis",
            "IL2060:MakeGenericMethod",
            Justification = "The referenced methods don't have any DynamicallyAccessedMembers annotations. See https://github.com/mono/linker/issues/1727")]
        [UnconditionalSuppressMessage(
            "ReflectionAnalysis",
            "IL2075:MakeGenericMethod",
            Justification = "The referenced methods don't have any DynamicallyAccessedMembers annotations. See https://github.com/mono/linker/issues/1727")]
        [UnconditionalSuppressMessage(
            "ReflectionAnalysis",
            "IL2076:MakeGenericMethod",
            Justification = "The referenced methods don't have any DynamicallyAccessedMembers annotations. See https://github.com/mono/linker/issues/1727")]
        public static BindFormatter<T> Get<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
        {
            if (!_cache.TryGetValue(typeof(T), out var formatter))
            {
                // We need to replicate all of the primitive cases that we handle here so that they will behave the same way.
                // The result will be cached.
                if (typeof(T) == typeof(string))
                {
                    formatter = (BindFormatter<string>)FormatStringValueCore;
                }
                else if (typeof(T) == typeof(bool))
                {
                    formatter = (BindFormatter<bool>)FormatBoolValueCore;
                }
                else if (typeof(T) == typeof(bool?))
                {
                    formatter = (BindFormatter<bool?>)FormatNullableBoolValueCore;
                }
                else if (typeof(T) == typeof(int))
                {
                    formatter = (BindFormatter<int>)FormatIntValueCore;
                }
                else if (typeof(T) == typeof(int?))
                {
                    formatter = (BindFormatter<int?>)FormatNullableIntValueCore;
                }
                else if (typeof(T) == typeof(long))
                {
                    formatter = (BindFormatter<long>)FormatLongValueCore;
                }
                else if (typeof(T) == typeof(long?))
                {
                    formatter = (BindFormatter<long?>)FormatNullableLongValueCore;
                }
                else if (typeof(T) == typeof(short))
                {
                    formatter = (BindFormatter<short>)FormatShortValueCore;
                }
                else if (typeof(T) == typeof(short?))
                {
                    formatter = (BindFormatter<short?>)FormatNullableShortValueCore;
                }
                else if (typeof(T) == typeof(float))
                {
                    formatter = (BindFormatter<float>)FormatFloatValueCore;
                }
                else if (typeof(T) == typeof(float?))
                {
                    formatter = (BindFormatter<float?>)FormatNullableFloatValueCore;
                }
                else if (typeof(T) == typeof(double))
                {
                    formatter = (BindFormatter<double>)FormatDoubleValueCore;
                }
                else if (typeof(T) == typeof(double?))
                {
                    formatter = (BindFormatter<double?>)FormatNullableDoubleValueCore;
                }
                else if (typeof(T) == typeof(decimal))
                {
                    formatter = (BindFormatter<decimal>)FormatDecimalValueCore;
                }
                else if (typeof(T) == typeof(decimal?))
                {
                    formatter = (BindFormatter<decimal?>)FormatNullableDecimalValueCore;
                }
                else if (typeof(T) == typeof(DateTime))
                {
                    formatter = (BindFormatter<DateTime>)FormatDateTimeValueCore;
                }
                else if (typeof(T) == typeof(DateTime?))
                {
                    formatter = (BindFormatter<DateTime?>)FormatNullableDateTimeValueCore;
                }
                else if (typeof(T) == typeof(DateTimeOffset))
                {
                    formatter = (BindFormatter<DateTimeOffset>)FormatDateTimeOffsetValueCore;
                }
                else if (typeof(T) == typeof(DateTimeOffset?))
                {
                    formatter = (BindFormatter<DateTimeOffset?>)FormatNullableDateTimeOffsetValueCore;
                }
                else if (typeof(T) == typeof(DateOnly))
                {
                    formatter = (BindFormatter<DateOnly>)FormatDateOnlyValueCore;
                }
                else if (typeof(T) == typeof(DateOnly?))
                {
                    formatter = (BindFormatter<DateOnly?>)FormatNullableDateOnlyValueCore;
                }
                else if (typeof(T) == typeof(TimeOnly))
                {
                    formatter = (BindFormatter<TimeOnly>)FormatTimeOnlyValueCore;
                }
                else if (typeof(T) == typeof(TimeOnly?))
                {
                    formatter = (BindFormatter<TimeOnly?>)FormatNullableTimeOnlyValueCore;
                }
                else if (typeof(T).IsEnum || Nullable.GetUnderlyingType(typeof(T)) is Type { IsEnum: true } innerType)
                {
                    formatter = (BindFormatter<T>)FormatEnumValueCore<T>;
                }
                else if (typeof(T).IsArray)
                {
                    var method = _makeArrayFormatter ??= typeof(FormatterDelegateCache).GetMethod(nameof(MakeArrayFormatter), BindingFlags.NonPublic | BindingFlags.Static)!;
                    var elementType = typeof(T).GetElementType()!;
                    formatter = (Delegate)method.MakeGenericMethod(elementType).Invoke(null, null)!;
                }
                else
                {
                    formatter = MakeTypeConverterFormatter<T>();
                }

                _cache.TryAdd(typeof(T), formatter);
            }

            return (BindFormatter<T>)formatter;
        }

        private static BindFormatter<T[]> MakeArrayFormatter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
        {
            var elementFormatter = Get<T>();

            return FormatArrayValue;

            string? FormatArrayValue(T[] value, CultureInfo? culture)
            {
                if (value.Length == 0)
                {
                    return "[]";
                }

                var builder = new StringBuilder("[\"");
                builder.Append(JsonEncodedText.Encode(elementFormatter(value[0], culture)?.ToString() ?? string.Empty).Value);
                builder.Append('\"');

                for (var i = 1; i < value.Length; i++)
                {
                    builder.Append(", \"");
                    builder.Append(JsonEncodedText.Encode(elementFormatter(value[i], culture)?.ToString() ?? string.Empty).Value);
                    builder.Append('\"');
                }

                builder.Append(']');

                return builder.ToString();
            }
        }

        [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect unknown underlying types are configured by application code to be retained.")]
        private static BindFormatter<T> MakeTypeConverterFormatter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
        {
            var typeConverter = TypeDescriptor.GetConverter(typeof(T));
            if (typeConverter == null || !typeConverter.CanConvertTo(typeof(string)))
            {
                throw new InvalidOperationException(
                    $"The type '{typeof(T).FullName}' does not have an associated {typeof(TypeConverter).Name} that supports " +
                    $"conversion to a string. " +
                    $"Apply '{typeof(TypeConverterAttribute).Name}' to the type to register a converter.");
            }

            return FormatWithTypeConverter;

            string? FormatWithTypeConverter(T value, CultureInfo? culture)
            {
                // We intentionally close-over the TypeConverter to cache it. The TypeDescriptor infrastructure is slow.
                return typeConverter.ConvertToString(context: null, culture ?? CultureInfo.CurrentCulture, value);
            }
        }
    }

    internal static class ParserDelegateCache
    {
        private static readonly ConcurrentDictionary<Type, Delegate> _cache = new ConcurrentDictionary<Type, Delegate>();

        private static MethodInfo? _convertToEnum;
        private static MethodInfo? _convertToNullableEnum;
        private static MethodInfo? _makeArrayTypeConverter;

        [UnconditionalSuppressMessage(
            "ReflectionAnalysis",
            "IL2060:MakeGenericMethod",
            Justification = "The referenced methods don't have any DynamicallyAccessedMembers annotations. See https://github.com/mono/linker/issues/1727")]
        [UnconditionalSuppressMessage(
            "ReflectionAnalysis",
            "IL2075:MakeGenericMethod",
            Justification = "The referenced methods don't have any DynamicallyAccessedMembers annotations. See https://github.com/mono/linker/issues/1727")]
        [UnconditionalSuppressMessage(
            "ReflectionAnalysis",
            "IL2076:MakeGenericMethod",
            Justification = "The referenced methods don't have any DynamicallyAccessedMembers annotations. See https://github.com/mono/linker/issues/1727")]
        public static BindParser<T> Get<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
        {
            if (!_cache.TryGetValue(typeof(T), out var parser))
            {
                // We need to replicate all of the primitive cases that we handle here so that they will behave the same way.
                // The result will be cached.
                if (typeof(T) == typeof(string))
                {
                    parser = ConvertToString;
                }
                else if (typeof(T) == typeof(bool))
                {
                    parser = ConvertToBool;
                }
                else if (typeof(T) == typeof(bool?))
                {
                    parser = ConvertToNullableBool;
                }
                else if (typeof(T) == typeof(int))
                {
                    parser = ConvertToInt;
                }
                else if (typeof(T) == typeof(int?))
                {
                    parser = ConvertToNullableInt;
                }
                else if (typeof(T) == typeof(long))
                {
                    parser = ConvertToLong;
                }
                else if (typeof(T) == typeof(long?))
                {
                    parser = ConvertToNullableLong;
                }
                else if (typeof(T) == typeof(short))
                {
                    parser = ConvertToShort;
                }
                else if (typeof(T) == typeof(short?))
                {
                    parser = ConvertToNullableShort;
                }
                else if (typeof(T) == typeof(float))
                {
                    parser = ConvertToFloat;
                }
                else if (typeof(T) == typeof(float?))
                {
                    parser = ConvertToNullableFloat;
                }
                else if (typeof(T) == typeof(double))
                {
                    parser = ConvertToDoubleDelegate;
                }
                else if (typeof(T) == typeof(double?))
                {
                    parser = ConvertToNullableDoubleDelegate;
                }
                else if (typeof(T) == typeof(decimal))
                {
                    parser = ConvertToDecimal;
                }
                else if (typeof(T) == typeof(decimal?))
                {
                    parser = ConvertToNullableDecimal;
                }
                else if (typeof(T) == typeof(DateTime))
                {
                    parser = ConvertToDateTime;
                }
                else if (typeof(T) == typeof(DateTime?))
                {
                    parser = ConvertToNullableDateTime;
                }
                else if (typeof(T) == typeof(DateTimeOffset))
                {
                    parser = ConvertToDateTimeOffset;
                }
                else if (typeof(T) == typeof(DateTimeOffset?))
                {
                    parser = ConvertToNullableDateTimeOffset;
                }
                else if (typeof(T) == typeof(DateOnly))
                {
                    parser = ConvertToDateOnly;
                }
                else if (typeof(T) == typeof(DateOnly?))
                {
                    parser = ConvertToNullableDateOnly;
                }
                else if (typeof(T) == typeof(TimeOnly))
                {
                    parser = ConvertToTimeOnly;
                }
                else if (typeof(T) == typeof(TimeOnly?))
                {
                    parser = ConvertToNullableTimeOnly;
                }
                else if (typeof(T) == typeof(Guid))
                {
                    parser = ConvertToGuid;
                }
                else if (typeof(T) == typeof(Guid?))
                {
                    parser = ConvertToNullableGuid;
                }
                else if (typeof(T).IsEnum)
                {
                    // We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse.
                    var method = _convertToEnum ??= typeof(BindConverter).GetMethod(nameof(ConvertToEnum), BindingFlags.NonPublic | BindingFlags.Static)!;
                    parser = method.MakeGenericMethod(typeof(T)).CreateDelegate(typeof(BindParser<T>), target: null);
                }
                else if (Nullable.GetUnderlyingType(typeof(T)) is Type innerType && innerType.IsEnum)
                {
                    // We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse.
                    var method = _convertToNullableEnum ??= typeof(BindConverter).GetMethod(nameof(ConvertToNullableEnum), BindingFlags.NonPublic | BindingFlags.Static)!;
                    parser = method.MakeGenericMethod(innerType).CreateDelegate(typeof(BindParser<T>), target: null);
                }
                else if (typeof(T).IsArray)
                {
                    var method = _makeArrayTypeConverter ??= typeof(ParserDelegateCache).GetMethod(nameof(MakeArrayTypeConverter), BindingFlags.NonPublic | BindingFlags.Static)!;
                    var elementType = typeof(T).GetElementType()!;
                    parser = (Delegate)method.MakeGenericMethod(elementType).Invoke(null, null)!;
                }
                else
                {
                    parser = MakeTypeConverterConverter<T>();
                }

                _cache.TryAdd(typeof(T), parser);
            }

            return (BindParser<T>)parser;
        }

        private static BindParser<T[]?> MakeArrayTypeConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
        {
            var elementParser = Get<T>();

            return ConvertToArray;

            bool ConvertToArray(object? obj, CultureInfo? culture, out T[]? value)
            {
                if (obj is not Array initialArray)
                {
                    value = default;
                    return false;
                }

                var convertedArray = new T[initialArray.Length];

                for (var i = 0; i < initialArray.Length; i++)
                {
                    if (!elementParser(initialArray.GetValue(i), culture, out convertedArray[i]!))
                    {
                        value = default;
                        return false;
                    }
                }

                value = convertedArray;
                return true;
            }
        }

        [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect unknown underlying types are configured by application code to be retained.")]
        private static BindParser<T> MakeTypeConverterConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
        {
            var typeConverter = TypeDescriptor.GetConverter(typeof(T));
            if (typeConverter == null || !typeConverter.CanConvertFrom(typeof(string)))
            {
                throw new InvalidOperationException(
                    $"The type '{typeof(T).FullName}' does not have an associated {typeof(TypeConverter).Name} that supports " +
                    $"conversion from a string. " +
                    $"Apply '{typeof(TypeConverterAttribute).Name}' to the type to register a converter.");
            }

            return ConvertWithTypeConverter;

            bool ConvertWithTypeConverter(object? obj, CultureInfo? culture, out T value)
            {
                // We intentionally close-over the TypeConverter to cache it. The TypeDescriptor infrastructure is slow.
                if (obj == null)
                {
                    value = default!;
                    return true;
                }
                var converted = typeConverter.ConvertFrom(context: null, culture ?? CultureInfo.CurrentCulture, obj);
                if (converted == null)
                {
                    value = default!;
                    return true;
                }

                value = (T)converted;
                return true;
            }
        }
    }
}
