// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

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(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(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 Attribute[] { 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 Attribute[] { 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)
        {
            Binding b = e.Element as Binding;

            switch (e.Action)
            {
                case CollectionChangeAction.Add:
                    b.BindingComplete += new BindingCompleteEventHandler(Binding_BindingComplete);
                    break;
                case CollectionChangeAction.Remove:
                    b.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);
        }
    }
}
