// 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;
using System.ComponentModel.Design.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace System.ComponentModel
{
    /// <summary>
    /// Converts the value of an object into a different data type.
    /// </summary>
    public class TypeConverter
    {
        internal const string RequiresUnreferencedCodeMessage = "Generic TypeConverters may require the generic types to be annotated. For example, NullableConverter requires the underlying type to be DynamicallyAccessedMembers All.";

        /// <summary>
        /// Gets a value indicating whether this converter can convert an object in the
        /// given source type to the native type of the converter.
        /// </summary>
        public bool CanConvertFrom(Type sourceType) => CanConvertFrom(null, sourceType);

        /// <summary>
        /// Gets a value indicating whether this converter can convert an object in the given
        /// source type to the native type of the converter using the context.
        /// </summary>
        public virtual bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
            => sourceType == typeof(InstanceDescriptor);

        /// <summary>
        /// Gets a value indicating whether this converter can convert an object to the given
        /// destination type using the context.
        /// </summary>
        public bool CanConvertTo([NotNullWhen(true)] Type? destinationType) => CanConvertTo(null, destinationType);

        /// <summary>
        /// Gets a value indicating whether this converter can convert an object to the given
        /// destination type using the context.
        /// </summary>
        public virtual bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
        {
            return destinationType == typeof(string);
        }

        /// <summary>
        /// Converts the given value to the converter's native type.
        /// </summary>
        public object? ConvertFrom(object value) => ConvertFrom(null, CultureInfo.CurrentCulture, value);

        /// <summary>
        /// Converts the given object to the converter's native type.
        /// </summary>
        public virtual object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
        {
            if (value is InstanceDescriptor instanceDescriptor)
            {
                return instanceDescriptor.Invoke();
            }
            throw GetConvertFromException(value);
        }

        /// <summary>
        /// Converts the given string to the converter's native type using the invariant culture.
        /// </summary>
        public object? ConvertFromInvariantString(string text)
        {
            return ConvertFromString(null, CultureInfo.InvariantCulture, text);
        }

        /// <summary>
        /// Converts the given string to the converter's native type using the invariant culture.
        /// </summary>
        public object? ConvertFromInvariantString(ITypeDescriptorContext? context, string text)
        {
            return ConvertFromString(context, CultureInfo.InvariantCulture, text);
        }

        /// <summary>
        /// Converts the specified text into an object.
        /// </summary>
        public object? ConvertFromString(string text) => ConvertFrom(null, null, text);

        /// <summary>
        /// Converts the specified text into an object.
        /// </summary>
        public object? ConvertFromString(ITypeDescriptorContext? context, string text)
        {
            return ConvertFrom(context, CultureInfo.CurrentCulture, text);
        }

        /// <summary>
        /// Converts the specified text into an object.
        /// </summary>
        public object? ConvertFromString(ITypeDescriptorContext? context, CultureInfo? culture, string text)
        {
            return ConvertFrom(context, culture, text);
        }

        /// <summary>
        /// Converts the given
        /// value object to the specified destination type using the arguments.
        /// </summary>
        public object? ConvertTo(object? value, Type destinationType)
        {
            return ConvertTo(null, null, value, destinationType);
        }

        /// <summary>
        /// Converts the given value object to
        /// the specified destination type using the specified context and arguments.
        /// </summary>
        public virtual object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
        {
            ArgumentNullException.ThrowIfNull(destinationType);

            if (destinationType == typeof(string))
            {
                if (value == null)
                {
                    return string.Empty;
                }

                if (culture != null && culture != CultureInfo.CurrentCulture)
                {
                    if (value is IFormattable formattable)
                    {
                        return formattable.ToString(format: null, formatProvider: culture);
                    }
                }
                return value.ToString();
            }
            throw GetConvertToException(value, destinationType);
        }

        /// <summary>
        /// Converts the specified value to a culture-invariant string representation.
        /// </summary>
        public string? ConvertToInvariantString(object? value)
        {
            return ConvertToString(null, CultureInfo.InvariantCulture, value);
        }

        /// <summary>
        /// Converts the specified value to a culture-invariant string representation.
        /// </summary>
        public string? ConvertToInvariantString(ITypeDescriptorContext? context, object? value)
        {
            return ConvertToString(context, CultureInfo.InvariantCulture, value);
        }

        /// <summary>
        /// Converts the specified value to a string representation.
        /// </summary>
        public string? ConvertToString(object? value)
        {
            return (string?)ConvertTo(null, CultureInfo.CurrentCulture, value, typeof(string));
        }

        /// <summary>
        /// Converts the specified value to a string representation.
        /// </summary>
        public string? ConvertToString(ITypeDescriptorContext? context, object? value)
        {
            return (string?)ConvertTo(context, CultureInfo.CurrentCulture, value, typeof(string));
        }

        /// <summary>
        /// Converts the specified value to a string representation.
        /// </summary>
        public string? ConvertToString(ITypeDescriptorContext? context, CultureInfo? culture, object? value)
        {
            return (string?)ConvertTo(context, culture, value, typeof(string));
        }

        /// <summary>
        /// Re-creates an <see cref='object'/> given a set of property values for the object.
        /// </summary>
        public object? CreateInstance(IDictionary propertyValues)
        {
            return CreateInstance(null, propertyValues);
        }

        /// <summary>
        /// Re-creates an <see cref='object'/> given a set of property values for the object.
        /// </summary>
        public virtual object? CreateInstance(ITypeDescriptorContext? context, IDictionary propertyValues) => null;

        /// <summary>
        /// Gets a suitable exception to throw when a conversion cannot be performed.
        /// </summary>
        protected Exception GetConvertFromException(object? value)
        {
            string? valueTypeName = value == null ? (SR.UsingResourceKeys() ? "(null)" : SR.Null) : value.GetType().FullName;
            throw new NotSupportedException(SR.Format(SR.ConvertFromException, GetType().Name, valueTypeName));
        }

        /// <summary>
        /// Retrieves a suitable exception to throw when a conversion cannot
        /// be performed.
        /// </summary>
        protected Exception GetConvertToException(object? value, Type destinationType)
        {
            string? valueTypeName = value == null ? (SR.UsingResourceKeys() ? "(null)" : SR.Null) : value.GetType().FullName;
            throw new NotSupportedException(SR.Format(SR.ConvertToException, GetType().Name, valueTypeName, destinationType.FullName));
        }

        /// <summary>
        /// Gets a value indicating whether changing a value on this object requires a call to
        /// <see cref='System.ComponentModel.TypeConverter.CreateInstance(IDictionary)'/> to create a new value.
        /// </summary>
        public bool GetCreateInstanceSupported() => GetCreateInstanceSupported(null);

        /// <summary>
        ///
        /// Gets a value indicating whether changing a value on this object requires a call to
        /// <see cref='System.ComponentModel.TypeConverter.CreateInstance(IDictionary)'/> to create a new value,
        /// using the specified context.
        ///
        /// </summary>
        public virtual bool GetCreateInstanceSupported(ITypeDescriptorContext? context) => false;

        /// <summary>
        /// Gets a collection of properties for the type of array specified by the value parameter.
        /// </summary>
        [RequiresUnreferencedCode("The Type of value cannot be statically discovered.")]
        public PropertyDescriptorCollection? GetProperties(object value) => GetProperties(null, value);

        /// <summary>
        ///
        /// Gets a collection of properties for the type of array specified by the value parameter using
        /// the specified context.
        ///
        /// </summary>
        [RequiresUnreferencedCode("The Type of value cannot be statically discovered.")]
        [DynamicDependency(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields, typeof(BrowsableAttribute))]
        public PropertyDescriptorCollection? GetProperties(ITypeDescriptorContext? context, object value)
        {
            return GetProperties(context, value, new Attribute[] { BrowsableAttribute.Yes });
        }

        /// <summary>
        ///
        /// Gets a collection of properties for the type of array specified by the value parameter using
        /// the specified context and attributes.
        ///
        /// </summary>
        [RequiresUnreferencedCode("The Type of value cannot be statically discovered. " + AttributeCollection.FilterRequiresUnreferencedCodeMessage)]
        public virtual PropertyDescriptorCollection? GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
        {
            return null;
        }

        /// <summary>
        /// Gets a value indicating whether this object supports properties.
        /// </summary>
        public bool GetPropertiesSupported() => GetPropertiesSupported(null);

        /// <summary>
        /// Gets a value indicating whether this object supports properties using the specified context.
        /// </summary>
        public virtual bool GetPropertiesSupported(ITypeDescriptorContext? context) => false;

        /// <summary>
        /// Gets a collection of standard values for the data type this type converter is designed for.
        /// </summary>
        public ICollection? GetStandardValues() => GetStandardValues(null);

        /// <summary>
        /// Gets a collection of standard values for the data type this type converter is designed for.
        /// </summary>
        public virtual StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) => null;

        /// <summary>
        /// Gets a value indicating whether the collection of standard values returned from
        /// <see cref='System.ComponentModel.TypeConverter.GetStandardValues()'/> is an exclusive list.
        /// </summary>
        public bool GetStandardValuesExclusive() => GetStandardValuesExclusive(null);

        /// <summary>
        /// Gets a value indicating whether the collection of standard values returned from
        /// <see cref='System.ComponentModel.TypeConverter.GetStandardValues()'/> is an exclusive
        /// list of possible values, using the specified context.
        /// </summary>
        public virtual bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => false;

        /// <summary>
        /// Gets a value indicating whether this object supports a standard set of values
        /// that can be picked from a list.
        /// </summary>
        public bool GetStandardValuesSupported() => GetStandardValuesSupported(null);

        /// <summary>
        /// Gets a value indicating whether this object supports a standard set of values that can be picked
        /// from a list using the specified context.
        /// </summary>
        public virtual bool GetStandardValuesSupported(ITypeDescriptorContext? context) => false;

        /// <summary>
        /// Gets a value indicating whether the given value object is valid for this type.
        /// </summary>
        public bool IsValid(object value) => IsValid(null, value);

        /// <summary>
        /// Gets a value indicating whether the given value object is valid for this type.
        /// </summary>
        public virtual bool IsValid(ITypeDescriptorContext? context, object value)
        {
            bool isValid = true;
            try
            {
                // Because null doesn't have a type, so we couldn't pass this to CanConvertFrom.
                // Meanwhile, we couldn't silence null value here, such as type converter like
                // NullableConverter would consider null value as a valid value.
                if (value == null || CanConvertFrom(context, value.GetType()))
                {
                    ConvertFrom(context, CultureInfo.InvariantCulture, value!);
                }
                else
                {
                    isValid = false;
                }
            }
            catch
            {
                isValid = false;
            }

            return isValid;
        }

        /// <summary>
        /// Sorts a collection of properties.
        /// </summary>
        protected PropertyDescriptorCollection SortProperties(PropertyDescriptorCollection props, string[] names)
        {
            props.Sort(names);
            return props;
        }

        /// <summary>
        /// An <see langword='abstract '/> class that provides properties for objects that do not have properties.
        /// </summary>
        protected abstract class SimplePropertyDescriptor : PropertyDescriptor
        {
            /// <summary>
            /// Initializes a new instance of the <see cref='System.ComponentModel.TypeConverter.SimplePropertyDescriptor'/> class.
            /// </summary>
            protected SimplePropertyDescriptor(Type componentType, string name, Type propertyType) : this(componentType, name, propertyType, Array.Empty<Attribute>())
            {
            }

            /// <summary>
            /// Initializes a new instance of the <see cref='System.ComponentModel.TypeConverter.SimplePropertyDescriptor'/> class.
            /// </summary>
            protected SimplePropertyDescriptor(Type componentType, string name, Type propertyType, Attribute[]? attributes) : base(name, attributes)
            {
                ComponentType = componentType;
                PropertyType = propertyType;
            }

            /// <summary>
            /// Gets the type of the component this property description is bound to.
            /// </summary>
            public override Type ComponentType { get; }

            /// <summary>
            /// Gets a value indicating whether this property is read-only.
            /// </summary>
            public override bool IsReadOnly
            {
                [DynamicDependency(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields, typeof(ReadOnlyAttribute))]
                [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The DynamicDependency ensures the correct members are preserved.")]
                get { return Attributes.Contains(ReadOnlyAttribute.Yes); }
            }

            /// <summary>
            /// Gets the type of the property.
            /// </summary>
            public override Type PropertyType { get; }

            /// <summary>
            /// Gets a value indicating whether resetting the component will change the value of the component.
            /// </summary>
            public override bool CanResetValue(object component)
            {
                DefaultValueAttribute? attr = (DefaultValueAttribute?)Attributes[typeof(DefaultValueAttribute)];
                if (attr == null)
                {
                    return false;
                }

                return attr.Value!.Equals(GetValue(component));
            }

            /// <summary>
            /// Resets the value for this property of the component.
            /// </summary>
            public override void ResetValue(object component)
            {
                DefaultValueAttribute? attr = (DefaultValueAttribute?)Attributes[typeof(DefaultValueAttribute)];
                if (attr != null)
                {
                    SetValue(component, attr.Value);
                }
            }

            /// <summary>
            /// Gets a value indicating whether the value of this property needs to be persisted.
            /// </summary>
            public override bool ShouldSerializeValue(object component) => false;
        }

        /// <summary>
        /// Represents a collection of values.
        /// </summary>
        public class StandardValuesCollection : ICollection
        {
            private readonly ICollection _values;
            private Array? _valueArray;

            /// <summary>
            ///
            /// Initializes a new instance of the <see cref='System.ComponentModel.TypeConverter.StandardValuesCollection'/> class.
            ///
            /// </summary>
            public StandardValuesCollection(ICollection? values)
            {
                values ??= Array.Empty<object>();

                if (values is Array a)
                {
                    _valueArray = a;
                }

                _values = values;
            }

            /// <summary>
            ///
            /// Gets the number of objects in the collection.
            ///
            /// </summary>
            public int Count
            {
                get
                {
                    if (_valueArray != null)
                    {
                        return _valueArray.Length;
                    }
                    else
                    {
                        return _values.Count;
                    }
                }
            }

            /// <summary>
            /// Gets the object at the specified index number.
            /// </summary>
            public object? this[int index]
            {
                get
                {
                    if (_valueArray != null)
                    {
                        return _valueArray.GetValue(index);
                    }
                    if (_values is IList list)
                    {
                        return list[index];
                    }
                    // No other choice but to enumerate the collection.
                    //
                    _valueArray = new object[_values.Count];
                    _values.CopyTo(_valueArray, 0);
                    return _valueArray.GetValue(index);
                }
            }

            /// <summary>
            /// Copies the contents of this collection to an array.
            /// </summary>
            public void CopyTo(Array array, int index) => _values.CopyTo(array, index);

            /// <summary>
            /// Gets an enumerator for this collection.
            /// </summary>
            public IEnumerator GetEnumerator() => _values.GetEnumerator();

            /// <summary>
            /// Determines if this collection is synchronized. The ValidatorCollection is not synchronized for
            /// speed. Also, since it is read-only, there is no need to synchronize it.
            /// </summary>
            bool ICollection.IsSynchronized => false;

            /// <summary>
            /// Retrieves the synchronization root for this collection. Because we are not synchronized,
            /// this returns null.
            /// </summary>
            object ICollection.SyncRoot => null!;
        }
    }
}
