// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.ComponentModel.Design;

/// <summary>
///  This service tracks individual designer events. The class itself
///  receives event information by direct calls from DesignerApplication.
///  Those wishing to replace this service may do so but need to override
///  the appropriate virtual methods on DesignerApplication.
/// </summary>
internal sealed class DesignerEventService : IDesignerEventService
{
    private static readonly object s_eventActiveDesignerChanged = new();
    private static readonly object s_eventDesignerCreated = new();
    private static readonly object s_eventDesignerDisposed = new();
    private static readonly object s_eventSelectionChanged = new();

    private List<IDesignerHost>? _designerList;          // read write list used as data for the collection
    private DesignerCollection? _designerCollection;     // public read only view of the above list
    private IDesignerHost? _activeDesigner;              // the currently active designer. Can be null
    private EventHandlerList? _events;                   // list of events. Can be null
    private bool _inTransaction;                        // true if we are in a transaction
    private bool _deferredSelChange;                    // true if we have a deferred selection change notification pending

    /// <summary>
    ///  Internal ctor to prevent semitrust from creating us.
    /// </summary>
    internal DesignerEventService()
    {
    }

    /// <summary>
    ///  This is called by the DesignerApplication class when
    ///  a designer is activated. The passed in designer can
    ///  be null to signify no designer is currently active.
    /// </summary>
    internal void OnActivateDesigner(DesignSurface? surface)
    {
        IDesignerHost? host = null;
        if (surface is not null)
        {
            host = surface.GetService(typeof(IDesignerHost)) as IDesignerHost;
            Debug.Assert(host is not null, "Design surface did not provide us with a designer host");

            // If the designer host is not in our collection, add it.
            if (host is not null && (_designerList is null || !_designerList.Contains(host)))
            {
                OnCreateDesigner(surface);
            }
        }

        if (_activeDesigner == host)
        {
            return;
        }

        IDesignerHost? oldDesigner = _activeDesigner;
        _activeDesigner = host;

        if (oldDesigner is not null)
        {
            SinkChangeEvents(oldDesigner, false);
        }

        if (_activeDesigner is not null)
        {
            SinkChangeEvents(_activeDesigner, true);
        }

        (_events?[s_eventActiveDesignerChanged] as ActiveDesignerEventHandler)?.Invoke(this, new ActiveDesignerEventArgs(oldDesigner, host));

        // Activating a new designer automatically pushes a new selection.
        //
        OnSelectionChanged(this, EventArgs.Empty);
    }

    /// <summary>
    ///  Called when a component is added or removed from the active designer.
    ///  We raise a selection change event here.
    /// </summary>
    private void OnComponentAddedRemoved(object? sender, ComponentEventArgs ce)
    {
        if (ce.Component?.Site?.Container is IDesignerHost { Loading: true })
        {
            _deferredSelChange = true;
            return;
        }

        OnSelectionChanged(this, EventArgs.Empty);
    }

    /// <summary>
    ///  Called when a component has changed on the active designer. Here
    ///  we grab the active selection service and see if the component that
    ///  has changed is also selected. If it is, then we raise a global
    ///  selection changed event.
    /// </summary>
    private void OnComponentChanged(object? sender, ComponentChangedEventArgs ce)
    {
        if (ce.Component is not IComponent comp)
        {
            return;
        }

        if (comp.Site.TryGetService(out ISelectionService? ss) && ss.GetComponentSelected(comp))
        {
            OnSelectionChanged(this, EventArgs.Empty);
        }
    }

    /// <summary>
    ///  This is called by the DesignerApplication class when
    ///  a designer is created. Activation generally follows.
    /// </summary>
    [MemberNotNull(nameof(_designerList))]
    internal void OnCreateDesigner(DesignSurface surface)
    {
        Debug.Assert(surface is not null, "DesignerApplication should not pass null here");
        IDesignerHost? host = surface.GetService<IDesignerHost>();
        Debug.Assert(host is not null, "Design surface did not provide us with a designer host");

        _designerList ??= [];
        _designerList.Add(host);

        // Hookup an object disposed handler on the design surface so we know when it's gone.
        surface.Disposed += OnDesignerDisposed;

        if (_events?[s_eventDesignerCreated] is DesignerEventHandler eh)
        {
            eh(this, new DesignerEventArgs(host));
        }
    }

    /// <summary>
    ///  Called by DesignSurface when it is about to be disposed.
    /// </summary>
    private void OnDesignerDisposed(object? sender, EventArgs e)
    {
        DesignSurface surface = (DesignSurface)sender!;
        surface.Disposed -= OnDesignerDisposed;

        // Detach the selection change and add/remove events, if we were monitoring such events
        SinkChangeEvents(surface, false);

        IDesignerHost? host = surface.GetService<IDesignerHost>();
        Debug.Assert(host is not null, "Design surface removed host too early in dispose");
        if (host is null)
        {
            return;
        }

        if (_events?[s_eventDesignerDisposed] is DesignerEventHandler eh)
        {
            eh(this, new DesignerEventArgs(host));
        }

        _designerList?.Remove(host);
    }

    /// <summary>
    ///  Called by the active designer's selection service when the selection changes.
    ///  Also called directly by us when the active designer changes, as this is
    ///  also a change to the global selection context.
    /// </summary>
    private void OnSelectionChanged(object? sender, EventArgs e)
    {
        if (_inTransaction)
        {
            _deferredSelChange = true;
            return;
        }

        if (_events?[s_eventSelectionChanged] is EventHandler eh)
        {
            eh(this, e);
        }
    }

    /// <summary>
    ///  Called by the designer host when it is done loading
    ///  Here we queue up selection notification.
    /// </summary>
    private void OnLoadComplete(object? sender, EventArgs e)
    {
        if (!_deferredSelChange)
        {
            return;
        }

        _deferredSelChange = false;
        OnSelectionChanged(this, EventArgs.Empty);
    }

    /// <summary>
    ///  Called by the designer host when it is entering or leaving a batch
    ///  operation. Here we queue up selection notification and we turn off
    ///  our UI.
    /// </summary>
    private void OnTransactionClosed(object? sender, DesignerTransactionCloseEventArgs e)
    {
        if (!e.LastTransaction)
        {
            return;
        }

        _inTransaction = false;
        if (!_deferredSelChange)
        {
            return;
        }

        _deferredSelChange = false;
        OnSelectionChanged(this, EventArgs.Empty);
    }

    /// <summary>
    ///  Called by the designer host when it is entering or leaving a batch
    ///  operation. Here we queue up selection notification and we turn off
    ///  our UI.
    /// </summary>
    private void OnTransactionOpened(object? sender, EventArgs e)
    {
        _inTransaction = true;
    }

    /// <summary>
    ///  Sinks or unsinks selection and component change events from the
    ///  provided service provider. We need to raise global selection change
    ///  notifications. A global selection change should be raised whenever
    ///  the selection of the active designer changes, whenever a component
    ///  is added or removed from the active designer, or whenever the
    ///  active designer itself changes.
    /// </summary>
    private void SinkChangeEvents(IServiceProvider provider, bool sink)
    {
        ISelectionService? ss = provider.GetService<ISelectionService>();
        IComponentChangeService? cs = provider.GetService<IComponentChangeService>();
        IDesignerHost? host = provider.GetService<IDesignerHost>();

        if (sink)
        {
            ss?.SelectionChanged += OnSelectionChanged;

            if (cs is not null)
            {
                ComponentEventHandler ce = new(OnComponentAddedRemoved);
                cs.ComponentAdded += ce;
                cs.ComponentRemoved += ce;
                cs.ComponentChanged += OnComponentChanged;
            }

            if (host is not null)
            {
                host.TransactionOpened += OnTransactionOpened;
                host.TransactionClosed += OnTransactionClosed;
                host.LoadComplete += OnLoadComplete;

                if (host.InTransaction)
                {
                    OnTransactionOpened(host, EventArgs.Empty);
                }
            }
        }
        else
        {
            ss?.SelectionChanged -= OnSelectionChanged;

            if (cs is not null)
            {
                ComponentEventHandler ce = new(OnComponentAddedRemoved);
                cs.ComponentAdded -= ce;
                cs.ComponentRemoved -= ce;
                cs.ComponentChanged -= OnComponentChanged;
            }

            if (host is not null)
            {
                host.TransactionOpened -= OnTransactionOpened;
                host.TransactionClosed -= OnTransactionClosed;
                host.LoadComplete -= OnLoadComplete;

                if (host.InTransaction)
                {
                    OnTransactionClosed(host, new(commit: false, lastTransaction: true));
                }
            }
        }
    }

    /// <summary>
    ///  Gets the currently active designer.
    /// </summary>
    IDesignerHost? IDesignerEventService.ActiveDesigner => _activeDesigner;

    /// <summary>
    ///  Gets or
    ///  sets a collection of running design documents in the development environment.
    /// </summary>
    [MemberNotNull(nameof(_designerList))]
    [MemberNotNull(nameof(_designerCollection))]
    DesignerCollection IDesignerEventService.Designers
    {
        get
        {
            _designerList ??= [];
            _designerCollection ??= new DesignerCollection(_designerList);
            return _designerCollection;
        }
    }

    /// <summary>
    ///  Adds an event that will be raised when the currently active designer
    ///  changes.
    /// </summary>
    event ActiveDesignerEventHandler? IDesignerEventService.ActiveDesignerChanged
    {
        [MemberNotNull(nameof(_events))]
        add
        {
            _events ??= new EventHandlerList();

            _events[s_eventActiveDesignerChanged] = Delegate.Combine(_events[s_eventActiveDesignerChanged], value);
        }
        remove
        {
            if (_events is null)
            {
                return;
            }

            _events[s_eventActiveDesignerChanged] = Delegate.Remove(_events[s_eventActiveDesignerChanged], value);
        }
    }

    /// <summary>
    ///  Adds an event that will be raised when a designer is created.
    /// </summary>
    event DesignerEventHandler? IDesignerEventService.DesignerCreated
    {
        [MemberNotNull(nameof(_events))]
        add
        {
            _events ??= new EventHandlerList();

            _events[s_eventDesignerCreated] = Delegate.Combine(_events[s_eventDesignerCreated], value);
        }
        remove
        {
            if (_events is null)
            {
                return;
            }

            _events[s_eventDesignerCreated] = Delegate.Remove(_events[s_eventDesignerCreated], value);
        }
    }

    /// <summary>
    ///  Adds an event that will be raised when a designer is disposed.
    /// </summary>
    event DesignerEventHandler? IDesignerEventService.DesignerDisposed
    {
        [MemberNotNull(nameof(_events))]
        add
        {
            _events ??= new EventHandlerList();

            _events[s_eventDesignerDisposed] = Delegate.Combine(_events[s_eventDesignerDisposed], value);
        }
        remove
        {
            if (_events is null)
            {
                return;
            }

            _events[s_eventDesignerDisposed] = Delegate.Remove(_events[s_eventDesignerDisposed], value);
        }
    }

    /// <summary>
    ///  Adds an event that will be raised when the global selection changes.
    /// </summary>
    event EventHandler? IDesignerEventService.SelectionChanged
    {
        [MemberNotNull(nameof(_events))]
        add
        {
            _events ??= new EventHandlerList();

            _events[s_eventSelectionChanged] = Delegate.Combine(_events[s_eventSelectionChanged], value);
        }
        remove
        {
            if (_events is null)
            {
                return;
            }

            _events[s_eventSelectionChanged] = Delegate.Remove(_events[s_eventSelectionChanged], value);
        }
    }
}
