// 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;
using System.ComponentModel.Design.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace System.Windows.Forms.Design;

internal partial class OleDragDropHandler
{
    protected class ComponentDataObject : IDataObject
    {
        private readonly IServiceProvider _serviceProvider;
        private object[]? _components;

        private MemoryStream? _serializationStream;
        private object? _serializationData;
        private CfCodeToolboxItem? _toolboxItemData;

        public ComponentDataObject(IServiceProvider serviceProvider, object[] components)
        {
            _serviceProvider = serviceProvider;
            _components = GetComponentList(components);
        }

        public ComponentDataObject(IServiceProvider serviceProvider, object serializationData)
        {
            _serviceProvider = serviceProvider;
            _serializationData = serializationData;
        }

        private MemoryStream? SerializationStream
        {
            get
            {
                if (_serializationStream is null && _components is not null)
                {
                    IDesignerSerializationService? ds = _serviceProvider.GetService<IDesignerSerializationService>();
                    if (ds is not null)
                    {
                        IComponent[] components = new IComponent[_components.Length];
                        for (int i = 0; i < _components.Length; i++)
                        {
                            Debug.Assert(_components[i] is IComponent, $"Item {_components[i].GetType().Name} is not an IComponent");
                            components[i] = (IComponent)_components[i];
                        }

                        object sd = ds.Serialize(components);
                        _serializationStream = new MemoryStream();
#pragma warning disable SYSLIB0011 // Type or member is obsolete
                        new BinaryFormatter().Serialize(_serializationStream, sd);
#pragma warning restore SYSLIB0011
                        _serializationStream.Seek(0, SeekOrigin.Begin);
                    }
                }

                return _serializationStream;
            }
        }

        public object[] Components
        {
            get
            {
                if (_components is null && (_serializationStream is not null || _serializationData is not null))
                {
                    Deserialize(null, false);
                }

                return (object[]?)_components?.Clone() ?? [];
            }
        }

        /// <summary>
        ///  Computes the IDataObject which constitutes this whole toolboxitem for storage in the toolbox.
        /// </summary>
        private CfCodeToolboxItem NestedToolboxItem => _toolboxItemData ??= new CfCodeToolboxItem(GetData(DataFormat));

        /// <summary>
        ///  Used to retrieve the selection for a copy. The default implementation
        ///  retrieves the current selection.
        /// </summary>
        private object[] GetComponentList(object[] components)
        {
            if (!_serviceProvider.TryGetService(out ISelectionService? selectionService))
            {
                return components;
            }

            ICollection selectedComponents;
            if (components is null)
                selectedComponents = selectionService.GetSelectedComponents();
            else
                selectedComponents = new List<object>(components);

            IDesignerHost? host = _serviceProvider.GetService<IDesignerHost>();
            if (host is not null)
            {
                List<IComponent> copySelection = [];
                foreach (IComponent comp in selectedComponents)
                {
                    copySelection.Add(comp);
                    GetAssociatedComponents(comp, host, copySelection);
                }

                selectedComponents = copySelection;
            }

            object[] comps = new object[selectedComponents.Count];
            selectedComponents.CopyTo(comps, 0);
            return comps;
        }

        private static void GetAssociatedComponents(IComponent component, IDesignerHost host, List<IComponent> list)
        {
            if (host.GetDesigner(component) is not ComponentDesigner designer)
            {
                return;
            }

            foreach (IComponent childComp in designer.AssociatedComponents)
            {
                list.Add(childComp);
                GetAssociatedComponents(childComp, host, list);
            }
        }

        public virtual object? GetData(string format)
        {
            return GetData(format, false);
        }

        public virtual object? GetData(string format, bool autoConvert)
        {
            if (format.Equals(DataFormat))
            {
                SerializationStream!.Seek(0, SeekOrigin.Begin);
#pragma warning disable SYSLIB0011 // Type or member is obsolete
#pragma warning disable CA2300 // Do not use insecure deserializer BinaryFormatter
#pragma warning disable CA2301 // Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize
                return new BinaryFormatter().Deserialize(SerializationStream); // CodeQL[SM03722, SM04191] : The operation is essential for the design experience when users are running their own designers they have created. This cannot be achieved without BinaryFormatter
#pragma warning restore CA2301
#pragma warning restore CA2300
#pragma warning restore SYSLIB0011
            }
            else if (format.Equals(NestedToolboxItemFormat))
            {
                NestedToolboxItem.SetDisplayName();
                return NestedToolboxItem;
            }

            return null;
        }

        public virtual object? GetData(Type t)
        {
            return GetData(t.FullName!);
        }

        /// <summary>
        ///  If the there is data store in the data object associated with
        ///  format this will return true.
        /// </summary>
        public bool GetDataPresent(string format, bool autoConvert)
        {
            return Array.IndexOf(GetFormats(), format) != -1;
        }

        /// <summary>
        ///  If the there is data store in the data object associated with
        ///  format this will return true.
        /// </summary>
        public bool GetDataPresent(string format)
        {
            return GetDataPresent(format, false);
        }

        /// <summary>
        ///  If the there is data store in the data object associated with
        ///  format this will return true.
        /// </summary>
        public bool GetDataPresent(Type format)
        {
            return GetDataPresent(format.FullName!, false);
        }

        /// <summary>
        ///  Retrieves a list of all formats stored in this data object.
        /// </summary>
        public string[] GetFormats(bool autoConvert)
        {
            return GetFormats();
        }

        /// <summary>
        ///  Retrieves a list of all formats stored in this data object.
        /// </summary>
        public string[] GetFormats()
        {
            return [NestedToolboxItemFormat, DataFormat, DataFormats.Serializable, ExtraInfoFormat];
        }

        [MemberNotNull(nameof(_components))]
        public void Deserialize(IServiceProvider? serviceProvider, bool removeCurrentComponents)
        {
            serviceProvider ??= _serviceProvider;

            IDesignerSerializationService ds = serviceProvider.GetService<IDesignerSerializationService>()!;
            IDesignerHost? host = null;
            DesignerTransaction? trans = null;

            try
            {
#pragma warning disable SYSLIB0011 // Type or member is obsolete
#pragma warning disable CA2300 // Do not use insecure deserializer BinaryFormatter
#pragma warning disable CA2301 // Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize
                _serializationData ??= new BinaryFormatter().Deserialize(SerializationStream!); // CodeQL[SM03722, SM04191] : The operation is essential for the design experience when users are running their own designers they have created. This cannot be achieved without BinaryFormatter
#pragma warning restore CA2301
#pragma warning restore CA2300
#pragma warning restore SYSLIB0011

                if (removeCurrentComponents && _components is not null)
                {
                    foreach (IComponent removeComp in _components)
                    {
                        if (host is null && removeComp.Site.TryGetService(out host))
                        {
                            trans = host.CreateTransaction(string.Format(SR.DragDropMoveComponents, _components.Length));
                        }

                        host?.DestroyComponent(removeComp);
                    }

                    _components = null;
                }

                ICollection objects = ds.Deserialize(_serializationData);
                _components = new object[objects.Count];
                objects.CopyTo(_components, 0);

                // only do top-level components here,
                // because other are already parented.
                // otherwise, when we process these
                // components it's too hard to know what we
                // should be reparenting.
                List<object> topComponents = new(_components.Length);
                for (int i = 0; i < _components.Length; i++)
                {
                    if (_components[i] is Control c)
                    {
                        if (c.Parent is null)
                        {
                            topComponents.Add(c);
                        }
                    }
                    else
                    {
                        topComponents.Add(_components[i]);
                    }
                }

                _components = [.. topComponents];
            }
            finally
            {
                trans?.Commit();
            }
        }

        /// <summary>
        ///  Sets the data to be associated with the specific data format. For
        ///  a listing of predefined formats see System.Windows.Forms.DataFormats.
        /// </summary>
        public void SetData(string format, bool autoConvert, object? data)
        {
            SetData(format, data);
        }

        /// <summary>
        ///  Sets the data to be associated with the specific data format. For
        ///  a listing of predefined formats see System.Windows.Forms.DataFormats.
        /// </summary>
        public void SetData(string format, object? data)
        {
            throw new InvalidOperationException(SR.DragDropSetDataError);
        }

        /// <summary>
        ///  Sets the data to be associated with the specific data format.
        /// </summary>
        public void SetData(Type format, object? data)
        {
            SetData(format.FullName!, data);
        }

        /// <summary>
        ///  Stores data in the data object. The format assumed is the
        ///  class of data
        /// </summary>
        public void SetData(object? data)
        {
            if (data is not null)
            {
                SetData(data.GetType(), data);
            }
        }
    }
}
