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

namespace System.Windows.Forms;

public class ListBindingConverter : TypeConverter
{
    private static Type[]? s_ctorTypes;  // the list of type of our ctor parameters.
    private static string?[]? s_ctorParamProps; // the name of each property to check to see if we need to init with a ctor.

    /// <summary>
    ///  Creates our array of types on demand.
    /// </summary>
    private static Type[] ConstructorParamaterTypes
    {
        get
        {
            s_ctorTypes ??= [typeof(string), typeof(object), typeof(string), typeof(bool), typeof(DataSourceUpdateMode), typeof(object), typeof(string), typeof(IFormatProvider)];

            return s_ctorTypes;
        }
    }

    /// <summary>
    ///  Creates our array of param names on demand.
    /// </summary>
    private static string?[] ConstructorParameterProperties
    {
        get
        {
            s_ctorParamProps ??= [null, null, null, "FormattingEnabled", "DataSourceUpdateMode", "NullValue", "FormatString", "FormatInfo",];

            return s_ctorParamProps;
        }
    }

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

        return base.CanConvertTo(context, destinationType);
    }

    /// <summary>
    ///  Converts the given object to another type. The most common types to convert
    ///  are to and from a string object. The default implementation will make a call
    ///  to ToString on the object if the object is valid and if the destination
    ///  type is string. If this cannot convert to the destination type, this will
    ///  throw a NotSupportedException.
    /// </summary>
    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        ArgumentNullException.ThrowIfNull(destinationType);

        if (destinationType == typeof(InstanceDescriptor) && value is Binding)
        {
            Binding b = (Binding)value;
            return GetInstanceDescriptorFromValues(b);
        }

        return base.ConvertTo(context, culture, value, destinationType);
    }

    /// <summary>
    ///  Creates an instance of this type given a set of property values
    ///  for the object. This is useful for objects that are immutable, but still
    ///  want to provide changeable properties.
    /// </summary>
    public override object CreateInstance(ITypeDescriptorContext? context, IDictionary propertyValues)
    {
        try
        {
            return new Binding(
                (string)propertyValues["PropertyName"]!,
                propertyValues["DataSource"],
                (string)propertyValues["DataMember"]!);
        }
        catch (InvalidCastException invalidCast)
        {
            throw new ArgumentException(SR.PropertyValueInvalidEntry, invalidCast);
        }
        catch (NullReferenceException nullRef)
        {
            throw new ArgumentException(SR.PropertyValueInvalidEntry, nullRef);
        }
    }

    /// <summary>
    ///  Determines if changing a value on this object should require a call to
    ///  CreateInstance to create a new value.
    /// </summary>
    public override bool GetCreateInstanceSupported(ITypeDescriptorContext? context)
    {
        return true;
    }

    /// <summary>
    ///  Gets the best matching ctor for a given binding and fills it out, based on the
    ///  state of the Binding and the optimal ctor.
    /// </summary>
    private static InstanceDescriptor GetInstanceDescriptorFromValues(Binding b)
    {
        // The BindingFormattingDialog turns on Binding::FormattingEnabled property.
        // however, when the user data binds a property using the PropertyBrowser,
        // Binding::FormattingEnabled is set to false.
        // The Binding class is not a component class, so we don't have the ComponentInitialize
        // method where we can set FormattingEnabled to true so we set it here.
        b.FormattingEnabled = true;

        bool isComplete = true;
        int lastItem = ConstructorParameterProperties.Length - 1;

        for (; lastItem >= 0; lastItem--)
        {
            // null means no prop is available, we quit here.
            if (ConstructorParameterProperties[lastItem] is null)
            {
                break;
            }

            // get the property and see if it needs to be serialized.
            string? constructorParameterProperty = ConstructorParameterProperties[lastItem];
            if (constructorParameterProperty is null)
            {
                break;
            }

            PropertyDescriptor? prop = TypeDescriptor.GetProperties(b)[constructorParameterProperty];
            if (prop is not null && prop.ShouldSerializeValue(b))
            {
                break;
            }
        }

        // now copy the type array up to the point we quit.
        Type[] ctorParams = new Type[lastItem + 1];
        Array.Copy(ConstructorParamaterTypes, 0, ctorParams, 0, ctorParams.Length);

        // Get the ctor info.
        ConstructorInfo? ctor = typeof(Binding).GetConstructor(ctorParams);
        Debug.Assert(ctor is not null, "Failed to find Binding ctor for types!");
        if (ctor is null)
        {
            isComplete = false;
            ctor = typeof(Binding).GetConstructor(
            [
                typeof(string),
                typeof(object),
                typeof(string)
            ]);
        }

        // now fill in the values.
        object?[] values = new object[ctorParams.Length];

        for (int i = 0; i < values.Length; i++)
        {
            object? val = null;
            switch (i)
            {
                case 0:
                    val = b.PropertyName;
                    break;
                case 1:
                    val = b.DataSource;
                    break;
                case 2:
                    val = b.BindingMemberInfo.BindingMember;
                    break;
                default:
                    string? constructorParameterProperty = ConstructorParameterProperties[i];
                    if (constructorParameterProperty is not null)
                    {
                        val = TypeDescriptor.GetProperties(b)[constructorParameterProperty]?.GetValue(b);
                    }

                    break;
            }

            values[i] = val;
        }

        return new InstanceDescriptor(ctor, values, isComplete);
    }
}
