﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Xml;
using Newtonsoft.Json;
using Umbraco.Core.Collections;

namespace Umbraco.Core
{
    /// <summary>
    /// Provides object extension methods.
    /// </summary>
    public static class ObjectExtensions
    {
        private static readonly ConcurrentDictionary<Type, Dictionary<string, object>> ToObjectTypes = new ConcurrentDictionary<Type, Dictionary<string, object>>();
        private static readonly ConcurrentDictionary<Type, Type> NullableGenericCache = new ConcurrentDictionary<Type, Type>();
        private static readonly ConcurrentDictionary<CompositeTypeTypeKey, TypeConverter> InputTypeConverterCache = new ConcurrentDictionary<CompositeTypeTypeKey, TypeConverter>();
        private static readonly ConcurrentDictionary<CompositeTypeTypeKey, TypeConverter> DestinationTypeConverterCache = new ConcurrentDictionary<CompositeTypeTypeKey, TypeConverter>();
        private static readonly ConcurrentDictionary<CompositeTypeTypeKey, bool> AssignableTypeCache = new ConcurrentDictionary<CompositeTypeTypeKey, bool>();
        private static readonly ConcurrentDictionary<Type, bool> BoolConvertCache = new ConcurrentDictionary<Type, bool>();

        private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' };
        private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter();

        //private static readonly ConcurrentDictionary<Type, Func<object>> ObjectFactoryCache = new ConcurrentDictionary<Type, Func<object>>();

        /// <summary>
        ///
        /// </summary>
        /// <param name="input"></param>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static IEnumerable<T> AsEnumerableOfOne<T>(this T input)
        {
            return Enumerable.Repeat(input, 1);
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="input"></param>
        public static void DisposeIfDisposable(this object input)
        {
            if (input is IDisposable disposable)
                disposable.Dispose();
        }

        /// <summary>
        /// Provides a shortcut way of safely casting an input when you cannot guarantee the <typeparamref name="T"/> is
        /// an instance type (i.e., when the C# AS keyword is not applicable).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="input">The input.</param>
        /// <returns></returns>
        internal static T SafeCast<T>(this object input)
        {
            if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default;
            if (input is T variable) return variable;
            return default;
        }

        /// <summary>
        /// Attempts to convert the input object to the output type.
        /// </summary>
        /// <remarks>This code is an optimized version of the original Umbraco method</remarks>
        /// <typeparam name="T">The type to convert to</typeparam>
        /// <param name="input">The input.</param>
        /// <returns>The <see cref="Attempt{T}"/></returns>
        public static Attempt<T> TryConvertTo<T>(this object input)
        {
            var result = TryConvertTo(input, typeof(T));

            if (result.Success)
                return Attempt<T>.Succeed((T)result.Result);

            // just try to cast
            try
            {
                return Attempt<T>.Succeed((T)input);
            }
            catch (Exception e)
            {
                return Attempt<T>.Fail(e);
            }
        }

        /// <summary>
        /// Attempts to convert the input object to the output type.
        /// </summary>
        /// <remarks>This code is an optimized version of the original Umbraco method</remarks>
        /// <param name="input">The input.</param>
        /// <param name="target">The type to convert to</param>
        /// <returns>The <see cref="Attempt{Object}"/></returns>
        public static Attempt<object> TryConvertTo(this object input, Type target)
        {
            if (target == null)
            {
                return Attempt<object>.Fail();
            }

            try
            {
                if (input == null)
                {
                    // Nullable is ok
                    if (target.IsGenericType && GetCachedGenericNullableType(target) != null)
                    {
                        return Attempt<object>.Succeed(null);
                    }

                    // Reference types are ok
                    return Attempt<object>.If(target.IsValueType == false, null);
                }

                var inputType = input.GetType();

                // Easy
                if (target == typeof(object) || inputType == target)
                {
                    return Attempt.Succeed(input);
                }

                // Check for string so that overloaders of ToString() can take advantage of the conversion.
                if (target == typeof(string))
                {
                    return Attempt<object>.Succeed(input.ToString());
                }

                // If we've got a nullable of something, we try to convert directly to that thing.
                // We cache the destination type and underlying nullable types
                // Any other generic types need to fall through
                if (target.IsGenericType)
                {
                    var underlying = GetCachedGenericNullableType(target);
                    if (underlying != null)
                    {
                        // Special case for empty strings for bools/dates which should return null if an empty string.
                        if (input is string inputString)
                        {
                            //TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null?
                            if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool)))
                            {
                                return Attempt<object>.Succeed(null);
                            }
                        }

                        // Recursively call into this method with the inner (not-nullable) type and handle the outcome
                        var inner = input.TryConvertTo(underlying);

                        // And if sucessful, fall on through to rewrap in a nullable; if failed, pass on the exception
                        if (inner.Success)
                        {
                            input = inner.Result; // Now fall on through...
                        }
                        else
                        {
                            return Attempt<object>.Fail(inner.Exception);
                        }
                    }
                }
                else
                {
                    // target is not a generic type

                    if (input is string inputString)
                    {
                        // Try convert from string, returns an Attempt if the string could be
                        // processed (either succeeded or failed), else null if we need to try
                        // other methods
                        var result = TryConvertToFromString(inputString, target);
                        if (result.HasValue)
                        {
                            return result.Value;
                        }
                    }

                    // TODO: Do a check for destination type being IEnumerable<T> and source type implementing IEnumerable<T> with
                    // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it.
                    if (GetCachedCanAssign(input, inputType, target))
                    {
                        return Attempt.Succeed(Convert.ChangeType(input, target));
                    }
                }

                if (target == typeof(bool))
                {
                    if (GetCachedCanConvertToBoolean(inputType))
                    {
                        return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input));
                    }
                }

                var inputConverter = GetCachedSourceTypeConverter(inputType, target);
                if (inputConverter != null)
                {
                    return Attempt.Succeed(inputConverter.ConvertTo(input, target));
                }

                var outputConverter = GetCachedTargetTypeConverter(inputType, target);
                if (outputConverter != null)
                {
                    return Attempt.Succeed(outputConverter.ConvertFrom(input));
                }

                if (target.IsGenericType && GetCachedGenericNullableType(target) != null)
                {
                    // cannot Convert.ChangeType as that does not work with nullable
                    // input has already been converted to the underlying type - just
                    // return input, there's an implicit conversion from T to T? anyways
                    return Attempt.Succeed(input);
                }

                // Re-check convertables since we altered the input through recursion
                if (input is IConvertible convertible2)
                {
                    return Attempt.Succeed(Convert.ChangeType(convertible2, target));
                }
            }
            catch (Exception e)
            {
                return Attempt<object>.Fail(e);
            }

            return Attempt<object>.Fail();
        }

        /// <summary>
        /// Attempts to convert the input string to the output type.
        /// </summary>
        /// <remarks>This code is an optimized version of the original Umbraco method</remarks>
        /// <param name="input">The input.</param>
        /// <param name="target">The type to convert to</param>
        /// <returns>The <see cref="Nullable{Attempt}"/></returns>
        private static Attempt<object>? TryConvertToFromString(this string input, Type target)
        {
            // Easy
            if (target == typeof(string))
            {
                return Attempt<object>.Succeed(input);
            }

            // Null, empty, whitespaces
            if (string.IsNullOrWhiteSpace(input))
            {
                if (target == typeof(bool))
                {
                    // null/empty = bool false
                    return Attempt<object>.Succeed(false);
                }

                if (target == typeof(DateTime))
                {
                    // null/empty = min DateTime value
                    return Attempt<object>.Succeed(DateTime.MinValue);
                }

                // Cannot decide here,
                // Any of the types below will fail parsing and will return a failed attempt
                // but anything else will not be processed and will return null
                // so even though the string is null/empty we have to proceed.
            }

            // Look for type conversions in the expected order of frequency of use.
            //
            // By using a mixture of ordered if statements and switches we can optimize both for
            // fast conditional checking for most frequently used types and the branching
            // that does not depend on previous values available to switch statements.
            if (target.IsPrimitive)
            {
                if (target == typeof(int))
                {
                    if (int.TryParse(input, out var value))
                    {
                        return Attempt<object>.Succeed(value);
                    }

                    // Because decimal 100.01m will happily convert to integer 100, it
                    // makes sense that string "100.01" *also* converts to integer 100.
                    var input2 = NormalizeNumberDecimalSeparator(input);
                    return Attempt<object>.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2));
                }

                if (target == typeof(long))
                {
                    if (long.TryParse(input, out var value))
                    {
                        return Attempt<object>.Succeed(value);
                    }

                    // Same as int
                    var input2 = NormalizeNumberDecimalSeparator(input);
                    return Attempt<object>.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2));
                }

                // TODO: Should we do the decimal trick for short, byte, unsigned?

                if (target == typeof(bool))
                {
                    if (bool.TryParse(input, out var value))
                    {
                        return Attempt<object>.Succeed(value);
                    }

                    // Don't declare failure so the CustomBooleanTypeConverter can try
                    return null;
                }

                // Calling this method directly is faster than any attempt to cache it.
                switch (Type.GetTypeCode(target))
                {
                    case TypeCode.Int16:
                        return Attempt<object>.If(short.TryParse(input, out var value), value);

                    case TypeCode.Double:
                        var input2 = NormalizeNumberDecimalSeparator(input);
                        return Attempt<object>.If(double.TryParse(input2, out var valueD), valueD);

                    case TypeCode.Single:
                        var input3 = NormalizeNumberDecimalSeparator(input);
                        return Attempt<object>.If(float.TryParse(input3, out var valueF), valueF);

                    case TypeCode.Char:
                        return Attempt<object>.If(char.TryParse(input, out var valueC), valueC);

                    case TypeCode.Byte:
                        return Attempt<object>.If(byte.TryParse(input, out var valueB), valueB);

                    case TypeCode.SByte:
                        return Attempt<object>.If(sbyte.TryParse(input, out var valueSb), valueSb);

                    case TypeCode.UInt32:
                        return Attempt<object>.If(uint.TryParse(input, out var valueU), valueU);

                    case TypeCode.UInt16:
                        return Attempt<object>.If(ushort.TryParse(input, out var valueUs), valueUs);

                    case TypeCode.UInt64:
                        return Attempt<object>.If(ulong.TryParse(input, out var valueUl), valueUl);
                }
            }
            else if (target == typeof(Guid))
            {
                return Attempt<object>.If(Guid.TryParse(input, out var value), value);
            }
            else if (target == typeof(DateTime))
            {
                if (DateTime.TryParse(input, out var value))
                {
                    switch (value.Kind)
                    {
                        case DateTimeKind.Unspecified:
                        case DateTimeKind.Utc:
                            return Attempt<object>.Succeed(value);

                        case DateTimeKind.Local:
                            return Attempt<object>.Succeed(value.ToUniversalTime());

                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }

                return Attempt<object>.Fail();
            }
            else if (target == typeof(DateTimeOffset))
            {
                return Attempt<object>.If(DateTimeOffset.TryParse(input, out var value), value);
            }
            else if (target == typeof(TimeSpan))
            {
                return Attempt<object>.If(TimeSpan.TryParse(input, out var value), value);
            }
            else if (target == typeof(decimal))
            {
                var input2 = NormalizeNumberDecimalSeparator(input);
                return Attempt<object>.If(decimal.TryParse(input2, out var value), value);
            }
            else if (input != null && target == typeof(Version))
            {
                return Attempt<object>.If(Version.TryParse(input, out var value), value);
            }

            // E_NOTIMPL IPAddress, BigInteger
			return null; // we can't decide...
        }
        internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname)
        {
            //TODO: Localise this exception
            if (isDisposed)
                throw new ObjectDisposedException(objectname);
        }

        //public enum PropertyNamesCaseType
        //{
        //    CamelCase,
        //    CaseInsensitive
        //}

        ///// <summary>
        ///// Convert an object to a JSON string with camelCase formatting
        ///// </summary>
        ///// <param name="obj"></param>
        ///// <returns></returns>
        //public static string ToJsonString(this object obj)
        //{
        //    return obj.ToJsonString(PropertyNamesCaseType.CamelCase);
        //}

        ///// <summary>
        ///// Convert an object to a JSON string with the specified formatting
        ///// </summary>
        ///// <param name="obj">The obj.</param>
        ///// <param name="propertyNamesCaseType">Type of the property names case.</param>
        ///// <returns></returns>
        //public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType)
        //{
        //    var type = obj.GetType();
        //    var dateTimeStyle = "yyyy-MM-dd HH:mm:ss";

        //    if (type.IsPrimitive || typeof(string).IsAssignableFrom(type))
        //    {
        //        return obj.ToString();
        //    }

        //    if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type))
        //    {
        //        return Convert.ToDateTime(obj).ToString(dateTimeStyle);
        //    }

        //    var serializer = new JsonSerializer();

        //    switch (propertyNamesCaseType)
        //    {
        //        case PropertyNamesCaseType.CamelCase:
        //            serializer.ContractResolver = new CamelCasePropertyNamesContractResolver();
        //            break;
        //    }

        //    var dateTimeConverter = new IsoDateTimeConverter
        //        {
        //            DateTimeStyles = System.Globalization.DateTimeStyles.None,
        //            DateTimeFormat = dateTimeStyle
        //        };

        //    if (typeof(IDictionary).IsAssignableFrom(type))
        //    {
        //        return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter);
        //    }

        //    if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type)))
        //    {
        //        return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter);
        //    }

        //    return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter);
        //}

        /// <summary>
        /// Converts an object into a dictionary
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="TProperty"></typeparam>
        /// <typeparam name="TVal"> </typeparam>
        /// <param name="o"></param>
        /// <param name="ignoreProperties"></param>
        /// <returns></returns>
        public static IDictionary<string, TVal> ToDictionary<T, TProperty, TVal>(this T o, params Expression<Func<T, TProperty>>[] ignoreProperties)
        {
            return o.ToDictionary<TVal>(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray());
        }

        /// <summary>
        /// Turns object into dictionary
        /// </summary>
        /// <param name="o"></param>
        /// <param name="ignoreProperties">Properties to ignore</param>
        /// <returns></returns>
        public static IDictionary<string, TVal> ToDictionary<TVal>(this object o, params string[] ignoreProperties)
        {
            if (o != null)
            {
                var props = TypeDescriptor.GetProperties(o);
                var d = new Dictionary<string, TVal>();
                foreach (var prop in props.Cast<PropertyDescriptor>().Where(x => ignoreProperties.Contains(x.Name) == false))
                {
                    var val = prop.GetValue(o);
                    if (val != null)
                    {
                        d.Add(prop.Name, (TVal)val);
                    }
                }
                return d;
            }
            return new Dictionary<string, TVal>();
        }

        /// <summary>
        /// Converts an object's properties into a dictionary.
        /// </summary>
        /// <param name="obj">The object to convert.</param>
        /// <param name="namer">A property namer function.</param>
        /// <returns>A dictionary containing each properties.</returns>
        public static Dictionary<string, object> ToObjectDictionary<T>(T obj, Func<PropertyInfo, string> namer = null)
        {
            if (obj == null) return new Dictionary<string, object>();

            string DefaultNamer(PropertyInfo property)
            {
                var jsonProperty = property.GetCustomAttribute<JsonPropertyAttribute>();
                return jsonProperty?.PropertyName ?? property.Name;
            }

            var t = obj.GetType();

            if (namer == null) namer = DefaultNamer;

            if (!ToObjectTypes.TryGetValue(t, out var properties))
            {
                properties = new Dictionary<string, object>();

                foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy))
                    properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter<T, object>(p);

                ToObjectTypes[t] = properties;
            }

            return properties.ToDictionary(x => x.Key, x => ((Func<T, object>) x.Value)(obj));
        }

        internal static string ToDebugString(this object obj, int levels = 0)
        {
            if (obj == null) return "{null}";
            try
            {
                if (obj is string)
                {
                    return "\"{0}\"".InvariantFormat(obj);
                }
                if (obj is int || obj is Int16 || obj is Int64 || obj is float || obj is double || obj is bool || obj is int? || obj is Int16? || obj is Int64? || obj is float? || obj is double? || obj is bool?)
                {
                    return "{0}".InvariantFormat(obj);
                }
                if (obj is Enum)
                {
                    return "[{0}]".InvariantFormat(obj);
                }
                if (obj is IEnumerable)
                {
                    var enumerable = (obj as IEnumerable);

                    var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList();

                    return items.Any()
                            ? "{{ {0} }}".InvariantFormat(string.Join(", ", items))
                            : null;
                }

                var props = obj.GetType().GetProperties();
                if ((props.Length == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2)
                {
                    try
                    {
                        var key = props[0].GetValue(obj, null) as string;
                        var value = props[1].GetValue(obj, null).ToDebugString(levels - 1);
                        return "{0}={1}".InvariantFormat(key, value);
                    }
                    catch (Exception)
                    {
                        return "[KeyValuePropertyException]";
                    }
                }
                if (levels > -1)
                {
                    var items =
                        (from propertyInfo in props
                         let value = GetPropertyDebugString(propertyInfo, obj, levels)
                         where value != null
                         select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray();

                    return items.Any()
                            ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items))
                            : null;
                }
            }
            catch (Exception ex)
            {
                return "[Exception:{0}]".InvariantFormat(ex.Message);
            }
            return null;
        }

        /// <summary>
        /// Attempts to serialize the value to an XmlString using ToXmlString
        /// </summary>
        /// <param name="value"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        internal static Attempt<string> TryConvertToXmlString(this object value, Type type)
        {
            try
            {
                var output = value.ToXmlString(type);
                return Attempt.Succeed(output);
            }
            catch (NotSupportedException ex)
            {
                return Attempt<string>.Fail(ex);
            }
        }

        /// <summary>
        /// Returns an XmlSerialized safe string representation for the value
        /// </summary>
        /// <param name="value"></param>
        /// <param name="type">The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown</param>
        /// <returns></returns>
        internal static string ToXmlString(this object value, Type type)
        {
            if (value == null) return string.Empty;
            if (type == typeof(string)) return (value.ToString().IsNullOrWhiteSpace() ? "" : value.ToString());
            if (type == typeof(bool)) return XmlConvert.ToString((bool)value);
            if (type == typeof(byte)) return XmlConvert.ToString((byte)value);
            if (type == typeof(char)) return XmlConvert.ToString((char)value);
            if (type == typeof(DateTime)) return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified);
            if (type == typeof(DateTimeOffset)) return XmlConvert.ToString((DateTimeOffset)value);
            if (type == typeof(decimal)) return XmlConvert.ToString((decimal)value);
            if (type == typeof(double)) return XmlConvert.ToString((double)value);
            if (type == typeof(float)) return XmlConvert.ToString((float)value);
            if (type == typeof(Guid)) return XmlConvert.ToString((Guid)value);
            if (type == typeof(int)) return XmlConvert.ToString((int)value);
            if (type == typeof(long)) return XmlConvert.ToString((long)value);
            if (type == typeof(sbyte)) return XmlConvert.ToString((sbyte)value);
            if (type == typeof(short)) return XmlConvert.ToString((short)value);
            if (type == typeof(TimeSpan)) return XmlConvert.ToString((TimeSpan)value);
            if (type == typeof(bool)) return XmlConvert.ToString((bool)value);
            if (type == typeof(uint)) return XmlConvert.ToString((uint)value);
            if (type == typeof(ulong)) return XmlConvert.ToString((ulong)value);
            if (type == typeof(ushort)) return XmlConvert.ToString((ushort)value);

            throw new NotSupportedException("Cannot convert type " + type.FullName + " to a string using ToXmlString as it is not supported by XmlConvert");
        }

        /// <summary>
        /// Returns an XmlSerialized safe string representation for the value and type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        internal static string ToXmlString<T>(this object value)
        {
            return value.ToXmlString(typeof (T));
        }

        private static string GetEnumPropertyDebugString(object enumItem, int levels)
        {
            try
            {
                return enumItem.ToDebugString(levels - 1);
            }
            catch (Exception)
            {
                return "[GetEnumPartException]";
            }
        }

        private static string GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels)
        {
            try
            {
                return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1);
            }
            catch (Exception)
            {
                return "[GetPropertyValueException]";
            }
        }

        internal static Guid AsGuid(this object value)
        {
            return value is Guid guid ? guid : Guid.Empty;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static string NormalizeNumberDecimalSeparator(string s)
        {
            var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0];
            return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized);
        }

        // gets a converter for source, that can convert to target, or null if none exists
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static TypeConverter GetCachedSourceTypeConverter(Type source, Type target)
        {
            var key = new CompositeTypeTypeKey(source, target);

            if (InputTypeConverterCache.TryGetValue(key, out var typeConverter))
            {
                return typeConverter;
            }

            var converter = TypeDescriptor.GetConverter(source);
            if (converter.CanConvertTo(target))
            {
                return InputTypeConverterCache[key] = converter;
            }

            return InputTypeConverterCache[key] = null;
        }

        // gets a converter for target, that can convert from source, or null if none exists
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static TypeConverter GetCachedTargetTypeConverter(Type source, Type target)
        {
            var key = new CompositeTypeTypeKey(source, target);

            if (DestinationTypeConverterCache.TryGetValue(key, out var typeConverter))
            {
                return typeConverter;
            }
            
            var converter = TypeDescriptor.GetConverter(target);
            if (converter.CanConvertFrom(source))
            {
                return DestinationTypeConverterCache[key] = converter;
            }

            return DestinationTypeConverterCache[key] = null;
        }

        // gets the underlying type of a nullable type, or null if the type is not nullable
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static Type GetCachedGenericNullableType(Type type)
        {
            if (NullableGenericCache.TryGetValue(type, out var underlyingType))
            {
                return underlyingType;
            }

            if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                Type underlying = Nullable.GetUnderlyingType(type);
                return NullableGenericCache[type] = underlying;
            }

            return NullableGenericCache[type] = null;
        }

        // gets an IConvertible from source to target type, or null if none exists
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool GetCachedCanAssign(object input, Type source, Type target)
        {
            var key = new CompositeTypeTypeKey(source, target);
            if (AssignableTypeCache.TryGetValue(key, out var canConvert))
            {
                return canConvert;
            }

            // "object is" is faster than "Type.IsAssignableFrom.
            // We can use it to very quickly determine whether true/false
            if (input is IConvertible && target.IsAssignableFrom(source))
            {
                return AssignableTypeCache[key] = true;
            }

            return AssignableTypeCache[key] = false;
        }

        // determines whether a type can be converted to boolean
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool GetCachedCanConvertToBoolean(Type type)
        {
            if (BoolConvertCache.TryGetValue(type, out var result))
            {
                return result;
            }

            if (CustomBooleanTypeConverter.CanConvertFrom(type))
            {
                return BoolConvertCache[type] = true;
            }

            return BoolConvertCache[type] = false;
        }

        
    }
}
