// 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.Reflection;

namespace System.Windows.Forms;

public abstract class BindingManagerBase
{
    private BindingsCollection? _bindings;
    private bool _pullingData;

    protected EventHandler? onCurrentChangedHandler; // Don't rename (breaking change)

    protected EventHandler? onPositionChangedHandler; // Don't rename (breaking change)

    // Hook BindingComplete events on all owned Binding objects, and propagate those events through our own BindingComplete event
    private BindingCompleteEventHandler? _onBindingCompleteHandler;

    // same deal about the new currentItemChanged event
    private protected EventHandler? _onCurrentItemChangedHandler;

    // Event handler for the DataError event
    private BindingManagerDataErrorEventHandler? _onDataErrorHandler;

    public BindingsCollection Bindings
    {
        get
        {
            if (_bindings is null)
            {
                _bindings = new ListManagerBindingsCollection(this);

                // Hook collection change events on collection, so we can hook or unhook the BindingComplete events on individual bindings
                _bindings.CollectionChanging += new CollectionChangeEventHandler(OnBindingsCollectionChanging);
                _bindings.CollectionChanged += new CollectionChangeEventHandler(OnBindingsCollectionChanged);
            }

            return _bindings;
        }
    }

    protected internal void OnBindingComplete(BindingCompleteEventArgs args)
    {
        _onBindingCompleteHandler?.Invoke(this, args);
    }

    protected internal abstract void OnCurrentChanged(EventArgs e);

    protected internal abstract void OnCurrentItemChanged(EventArgs e);

    protected internal void OnDataError(Exception e)
    {
        _onDataErrorHandler?.Invoke(this, new BindingManagerDataErrorEventArgs(e));
    }

    public abstract object? Current { get; }

    private protected abstract void SetDataSource(object? dataSource);

    public BindingManagerBase() { }

    internal BindingManagerBase(object? dataSource)
    {
        SetDataSource(dataSource);
    }

    internal abstract Type BindType { get; }

    internal abstract PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[]? listAccessors);

    public virtual PropertyDescriptorCollection GetItemProperties() => GetItemProperties(listAccessors: null);

    protected internal virtual PropertyDescriptorCollection? GetItemProperties(ArrayList dataSources, ArrayList listAccessors)
    {
        IList? list = null;
        if (this is CurrencyManager currencyManager)
        {
            list = currencyManager.List;
        }

        if (list is ITypedList typedList)
        {
            PropertyDescriptor[] properties = new PropertyDescriptor[listAccessors.Count];
            listAccessors.CopyTo(properties, 0);
            return typedList.GetItemProperties(properties);
        }

        return GetItemProperties(BindType, 0, dataSources, listAccessors);
    }

    protected virtual PropertyDescriptorCollection? GetItemProperties(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type listType,
        int offset,
        ArrayList dataSources,
        ArrayList listAccessors)
    {
        if (listAccessors.Count < offset)
        {
            return null;
        }

        if (listAccessors.Count == offset)
        {
            if (!typeof(IList).IsAssignableFrom(listType))
            {
                return TypeDescriptor.GetProperties(listType);
            }

            foreach (PropertyInfo property in listType.GetProperties())
            {
                if (property.Name == "Item" && property.PropertyType != typeof(object))
                {
                    return TypeDescriptor.GetProperties(property.PropertyType, [new BrowsableAttribute(true)]);
                }
            }

            // return the properties on the type of the first element in the list
            if (dataSources[offset - 1] is IList list && list.Count > 0)
            {
                return TypeDescriptor.GetProperties(list[0]!);
            }

            return null;
        }

        if (typeof(IList).IsAssignableFrom(listType))
        {
            PropertyDescriptorCollection? itemProps = null;
            foreach (PropertyInfo property in listType.GetProperties())
            {
                if (property.Name == "Item" && property.PropertyType != typeof(object))
                {
                    // get all the properties that are not marked as Browsable(false)
                    itemProps = TypeDescriptor.GetProperties(property.PropertyType, [new BrowsableAttribute(true)]);
                }
            }

            if (itemProps is null)
            {
                // Use the properties on the type of the first element in the list
                // if offset == 0, then this means that the first dataSource did not have a strongly typed Item property.
                // the dataSources are added only for relatedCurrencyManagers, so in this particular case
                // we need to use the dataSource in the currencyManager.
                IList? list;
                if (offset == 0)
                {
                    list = DataSource as IList;
                }
                else
                {
                    list = dataSources[offset - 1] as IList;
                }

                if (list is not null && list.Count > 0)
                {
                    itemProps = TypeDescriptor.GetProperties(list[0]!);
                }
            }

            if (itemProps is not null)
            {
                for (int j = 0; j < itemProps.Count; j++)
                {
                    if (itemProps[j].Equals(listAccessors[offset]))
                    {
                        return GetItemProperties(itemProps[j].PropertyType, offset + 1, dataSources, listAccessors);
                    }
                }
            }
        }
        else
        {
            foreach (PropertyInfo property in listType.GetProperties())
            {
                if (property.Name.Equals(((PropertyDescriptor)listAccessors[offset]!).Name))
                {
                    return GetItemProperties(property.PropertyType, offset + 1, dataSources, listAccessors);
                }
            }
        }

        return null;
    }

    public event BindingCompleteEventHandler BindingComplete
    {
        add => _onBindingCompleteHandler += value;
        remove => _onBindingCompleteHandler -= value;
    }

    public event EventHandler CurrentChanged
    {
        add => onCurrentChangedHandler += value;
        remove => onCurrentChangedHandler -= value;
    }

    public event EventHandler CurrentItemChanged
    {
        add => _onCurrentItemChangedHandler += value;
        remove => _onCurrentItemChangedHandler -= value;
    }

    public event BindingManagerDataErrorEventHandler DataError
    {
        add => _onDataErrorHandler += value;
        remove => _onDataErrorHandler -= value;
    }

    internal abstract string GetListName();
    public abstract void CancelCurrentEdit();
    public abstract void EndCurrentEdit();

    public abstract void AddNew();
    public abstract void RemoveAt(int index);

    public abstract int Position { get; set; }

    public event EventHandler PositionChanged
    {
        add => onPositionChangedHandler += value;
        remove => onPositionChangedHandler -= value;
    }

    protected abstract void UpdateIsBinding();

    protected internal abstract string GetListName(ArrayList? listAccessors);

    public abstract void SuspendBinding();

    public abstract void ResumeBinding();

    protected void PullData() => PullData(out _);

    internal void PullData(out bool success)
    {
        success = true;
        _pullingData = true;

        try
        {
            UpdateIsBinding();

            int numLinks = Bindings.Count;
            for (int i = 0; i < numLinks; i++)
            {
                if (Bindings[i].PullData())
                {
                    success = false;
                }
            }
        }
        finally
        {
            _pullingData = false;
        }
    }

    protected void PushData()
    {
        if (_pullingData)
        {
            return;
        }

        UpdateIsBinding();

        int numLinks = Bindings.Count;
        for (int i = 0; i < numLinks; i++)
        {
            Bindings[i].PushData();
        }
    }

    internal abstract object? DataSource { get; }

    internal abstract bool IsBinding { get; }

    public bool IsBindingSuspended => !IsBinding;

    public abstract int Count { get; }

    /// <summary>
    ///  BindingComplete events on individual Bindings are propagated up through the BindingComplete event on
    ///  the owning BindingManagerBase. To do this, we have to track changes to the bindings collection, adding
    ///  or removing handlers on items in the collection as appropriate.
    ///
    ///  For the Add and Remove cases, we hook the collection 'changed' event, and add or remove handler for
    ///  specific binding.
    ///
    ///  For the Refresh case, we hook both the 'changing' and 'changed' events, removing handlers for all
    ///  items that were in the collection before the change, then adding handlers for whatever items are
    ///  in the collection after the change.
    /// </summary>
    private void OnBindingsCollectionChanged(object? sender, CollectionChangeEventArgs e)
    {
        if (e.Element is not Binding binding)
        {
            return;
        }

        switch (e.Action)
        {
            case CollectionChangeAction.Add:
                binding.BindingComplete += new BindingCompleteEventHandler(Binding_BindingComplete);
                break;
            case CollectionChangeAction.Remove:
                binding.BindingComplete -= new BindingCompleteEventHandler(Binding_BindingComplete);
                break;
            case CollectionChangeAction.Refresh:
                foreach (Binding bi in Bindings)
                {
                    bi.BindingComplete += new BindingCompleteEventHandler(Binding_BindingComplete);
                }

                break;
        }
    }

    private void OnBindingsCollectionChanging(object? sender, CollectionChangeEventArgs e)
    {
        if (e.Action != CollectionChangeAction.Refresh)
        {
            return;
        }

        foreach (Binding bi in Bindings)
        {
            bi.BindingComplete -= new BindingCompleteEventHandler(Binding_BindingComplete);
        }
    }

    private void Binding_BindingComplete(object? sender, BindingCompleteEventArgs args)
    {
        OnBindingComplete(args);
    }
}
