// 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.Drawing;
using Microsoft.Win32;

namespace System.Windows.Forms.Design;

/// <summary>
///  The selection manager handles selection within a form. There is one selection manager for each form
///  or top level designer. A selection consists of an array of components. One component is designated the
///  "primary" selection and is displayed with different grab handles. An individual selection may or
///  may not have UI associated with it. If the selection manager can find a suitable designer that is representing
///  the selection, it will highlight the designer's border. If the merged property set has a location property,
///  the selection's rules will allow movement. Also, if the property set has a size property,
///  the selection's rules will allow for sizing. Grab handles may be drawn around the designer and
///  user interactions involving the selection frame and grab handles are initiated here, but
///  the actual movement of the objects is done in a designer object that implements the ISelectionHandler interface.
/// </summary>
internal sealed partial class SelectionUIService : Control, ISelectionUIService
{
    private static readonly Point s_invalidPoint = new(int.MinValue, int.MinValue);
    private const int HITTEST_CONTAINER_SELECTOR = 0x0001;
    private const int HITTEST_NORMAL_SELECTION = 0x0002;
    private const int HITTEST_DEFAULT = HITTEST_CONTAINER_SELECTOR | HITTEST_NORMAL_SELECTION;

    // These are used during a drag operation, either through our own handle drag or through ISelectionUIService
    private ISelectionUIHandler? _dragHandler; // the current drag handler
    private object[]? _dragComponents; // the controls being dragged
    private SelectionRules _dragRules; // movement constraints for the drag
    private bool _dragMoved;
    private object? _containerDrag; // object being dragged during a container drag
    // These are used during a drag of a selection grab handle
    private bool _ignoreCaptureChanged;
    private int _mouseDragHitTest; // where the hit occurred that caused the drag
    private Point _mouseDragAnchor = s_invalidPoint; // anchor point of the drag
    private Rectangle _mouseDragOffset = Rectangle.Empty; // current drag offset
    private Point _lastMoveScreenCoord = Point.Empty;
    private bool _ctrlSelect; // was the CTRL key down when the drag began
    private bool _mouseDragging; // Are we actually doing a drag?
    private ContainerSelectorActiveEventHandler? _containerSelectorActive; // the event we fire when user interacts with container selector
    private Dictionary<object, SelectionUIItem> _selectionItems;
    private readonly Dictionary<object, ISelectionUIHandler> _selectionHandlers; // Component UI handlers

    private bool _savedVisible; // we stash this when we mess with visibility ourselves.
    private bool _batchMode;
    private bool _batchChanged;
    private bool _batchSync;
    private readonly ISelectionService? _selSvc;
    private readonly IDesignerHost _host;
    private DesignerTransaction? _dragTransaction;

    /// <summary>
    ///  Creates a new selection manager object. The selection manager manages all selection of all designers
    ///  under the current form file.
    /// </summary>
    public SelectionUIService(IDesignerHost host) : base()
    {
        SetStyle(ControlStyles.StandardClick | ControlStyles.Opaque | ControlStyles.OptimizedDoubleBuffer, true);
        _host = host;
        _dragHandler = null;
        _dragComponents = null;
        _selectionItems = [];
        _selectionHandlers = [];
        AllowDrop = true;
        // Not really any reason for this, except that it can be handy when using Spy++
        Text = "SelectionUIOverlay";

        _selSvc = host.GetService<ISelectionService>();
        _selSvc?.SelectionChanged += OnSelectionChanged;

        // And configure the events we want to listen to.
        host.TransactionOpened += OnTransactionOpened;
        host.TransactionClosed += OnTransactionClosed;
        if (host.InTransaction)
        {
            OnTransactionOpened(host, EventArgs.Empty);
        }

        // Listen to the SystemEvents so that we can resync selection based on display settings etc.
        SystemEvents.DisplaySettingsChanged += OnSystemSettingChanged;
        SystemEvents.InstalledFontsChanged += OnSystemSettingChanged;
        SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
    }

    /// <summary>
    ///  override of control.
    /// </summary>
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.Style &= ~(int)(WINDOW_STYLE.WS_CLIPSIBLINGS | WINDOW_STYLE.WS_CLIPCHILDREN);
            return cp;
        }
    }

    /// <summary>
    ///  Called to initiate a mouse drag on the selection overlay. We cache some state here.
    /// </summary>
    private void BeginMouseDrag(Point anchor, int hitTest)
    {
        Capture = true;
        _ignoreCaptureChanged = false;
        _mouseDragAnchor = anchor;
        _mouseDragging = true;
        _mouseDragHitTest = hitTest;
        _mouseDragOffset = default;
        _savedVisible = Visible;
    }

    /// <summary>
    ///  Displays the given exception to the user.
    /// </summary>
    private void DisplayError(Exception e)
    {
        IUIService? uis = _host.GetService<IUIService>();
        if (uis is not null)
        {
            uis.ShowError(e);
        }
        else
        {
            string message = e.Message;
            if (string.IsNullOrEmpty(message))
            {
                message = e.ToString();
            }

            RTLAwareMessageBox.Show(null, message, null, MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1, 0);
        }
    }

    /// <summary>
    ///  Disposes the entire selection UI manager.
    /// </summary>
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _selSvc?.SelectionChanged -= OnSelectionChanged;

            if (_host is not null)
            {
                _host.TransactionOpened -= OnTransactionOpened;
                _host.TransactionClosed -= OnTransactionClosed;
                if (_host.InTransaction)
                {
                    OnTransactionClosed(_host, new DesignerTransactionCloseEventArgs(true, true));
                }
            }

            foreach (SelectionUIItem s in _selectionItems.Values)
            {
                s.Dispose();
            }

            _selectionHandlers.Clear();
            _selectionItems.Clear();
            // Listen to the SystemEvents so that we can resync selection based on display settings etc.
            SystemEvents.DisplaySettingsChanged -= OnSystemSettingChanged;
            SystemEvents.InstalledFontsChanged -= OnSystemSettingChanged;
            SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
        }

        base.Dispose(disposing);
    }

    /// <summary>
    ///  Called when we want to finish a mouse drag and clean up our variables. We call this from multiple places,
    ///  depending on the state of the finish. This does NOT end the drag -- for that must call EndDrag.
    ///  This just cleans up the state of the mouse.
    /// </summary>
    private void EndMouseDrag(Point position)
    {
        // it's possible for us to be destroyed in a drag -- e.g. if this is the tray's SelectionUIService and
        // the last item is dragged out, so check disposed first.
        if (IsDisposed)
        {
            return;
        }

        _ignoreCaptureChanged = true;
        Capture = false;
        _mouseDragAnchor = s_invalidPoint;
        _mouseDragOffset = Rectangle.Empty;
        _mouseDragHitTest = 0;
        _dragMoved = false;
        SetSelectionCursor(position);
        _mouseDragging = _ctrlSelect = false;
    }

    /// <summary>
    ///  Determines the selection hit test at the given point. The point should be in screen coordinates.
    /// </summary>
    private HitTestInfo GetHitTest(Point value, int flags)
    {
        Point pt = PointToClient(value);
        foreach (SelectionUIItem item in _selectionItems.Values)
        {
            if ((flags & HITTEST_CONTAINER_SELECTOR) != 0)
            {
                if (item is ContainerSelectionUIItem && (item.GetRules() & SelectionRules.Visible) != SelectionRules.None)
                {
                    int hitTest = item.GetHitTest(pt);
                    if ((hitTest & SelectionUIItem.CONTAINER_SELECTOR) != 0)
                    {
                        return new HitTestInfo(hitTest, item, true);
                    }
                }
            }

            if ((flags & HITTEST_NORMAL_SELECTION) != 0)
            {
                if (item is not ContainerSelectionUIItem && (item.GetRules() & SelectionRules.Visible) != SelectionRules.None)
                {
                    int hitTest = item.GetHitTest(pt);
                    if (hitTest != SelectionUIItem.NOHIT)
                    {
                        if (hitTest != 0)
                        {
                            return new HitTestInfo(hitTest, item);
                        }
                        else
                        {
                            return new HitTestInfo(SelectionUIItem.NOHIT, item);
                        }
                    }
                }
            }
        }

        return new HitTestInfo(SelectionUIItem.NOHIT, null);
    }

    private ISelectionUIHandler? GetHandler(object component)
        => _selectionHandlers.TryGetValue(component, out ISelectionUIHandler? value) ? value : null;

    /// <summary>
    ///  This method returns a well-formed name for a drag transaction based on the rules it is given.
    /// </summary>
    public static string GetTransactionName(SelectionRules rules, object[] objects)
    {
        // Determine a nice name for the drag operation
        string transactionName;
        if ((rules & SelectionRules.Moveable) != 0)
        {
            if (objects.Length > 1)
            {
                transactionName = string.Format(SR.DragDropMoveComponents, objects.Length);
            }
            else
            {
                string? name = string.Empty;
                if (objects.Length > 0)
                {
                    if (objects[0] is IComponent { Site: { } site })
                    {
                        name = site.Name;
                    }
                    else
                    {
                        name = objects[0].GetType().Name;
                    }
                }

                transactionName = string.Format(SR.DragDropMoveComponent, name);
            }
        }
        else if ((rules & SelectionRules.AllSizeable) != 0)
        {
            if (objects.Length > 1)
            {
                transactionName = string.Format(SR.DragDropSizeComponents, objects.Length);
            }
            else
            {
                string? name = string.Empty;
                if (objects.Length > 0)
                {
                    if (objects[0] is IComponent { Site: { } site })
                    {
                        name = site.Name;
                    }
                    else
                    {
                        name = objects[0].GetType().Name;
                    }
                }

                transactionName = string.Format(SR.DragDropSizeComponent, name);
            }
        }
        else
        {
            transactionName = string.Format(SR.DragDropDragComponents, objects.Length);
        }

        return transactionName;
    }

    /// <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)
        {
            _batchMode = false;
            if (_batchChanged)
            {
                _batchChanged = false;
                ((ISelectionUIService)this).SyncSelection();
            }

            if (_batchSync)
            {
                _batchSync = false;
                ((ISelectionUIService)this).SyncComponent(null);
            }
        }
    }

    /// <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)
    {
        _batchMode = true;
    }

    /// <summary>
    ///  update our window region on first create. We shouldn't do this before the handle is created or else we will force creation.
    /// </summary>
    protected override void OnHandleCreated(EventArgs e)
    {
        Debug.Assert(!RecreatingHandle, "Perf hit: we are recreating the docwin handle");
        base.OnHandleCreated(e);
        // Default the shape of the control to be empty, so that if nothing is initially selected that our window
        // surface doesn't interfere.
        UpdateWindowRegion();
    }

    /// <summary>
    ///  Called whenever a component changes. Here we update our selection information so that the
    ///  selection rectangles are all up to date.
    /// </summary>
    private void OnComponentChanged(object? sender, ComponentChangedEventArgs ccevent)
    {
        if (!_batchMode)
        {
            ((ISelectionUIService)this).SyncSelection();
        }
        else
        {
            _batchChanged = true;
        }
    }

    /// <summary>
    ///  called by the formcore when someone has removed a component.
    ///  This will remove any selection on the component without disturbing the rest of the selection.
    /// </summary>
    private void OnComponentRemove(object? sender, ComponentEventArgs ce)
    {
        _selectionHandlers.Remove(ce.Component!);
        _selectionItems.Remove(ce.Component!);
        ((ISelectionUIService)this).SyncComponent(ce.Component);
    }

    /// <summary>
    ///  Called to invoke the container active event, if a designer has bound to it.
    /// </summary>
    private void OnContainerSelectorActive(ContainerSelectorActiveEventArgs e)
    {
        _containerSelectorActive?.Invoke(this, e);
    }

    /// <summary>
    ///  Called when the selection changes. We sync up the UI with the selection at this point.
    /// </summary>
    private void OnSelectionChanged(object? sender, EventArgs e)
    {
        ICollection selection = _selSvc!.GetSelectedComponents();
        Dictionary<object, SelectionUIItem> newSelection = new(selection.Count);
        bool shapeChanged = false;
        foreach (object comp in selection)
        {
            bool create = true;
            if (_selectionItems.TryGetValue(comp, out SelectionUIItem? existingItem))
            {
                if (existingItem is ContainerSelectionUIItem item)
                {
                    item.Dispose();
                    shapeChanged = true;
                }
                else
                {
                    newSelection[comp] = existingItem;
                    create = false;
                }
            }

            if (create)
            {
                shapeChanged = true;
                newSelection[comp] = new SelectionUIItem(this, comp);
            }
        }

        if (!shapeChanged)
        {
            shapeChanged = _selectionItems.Keys.Count != newSelection.Keys.Count;
        }

        _selectionItems = newSelection;

        if (shapeChanged)
        {
            UpdateWindowRegion();
        }

        Invalidate();
        Update();
    }

    /// <summary>
    ///  User setting requires that we repaint.
    /// </summary>
    private void OnSystemSettingChanged(object? sender, EventArgs e) => Invalidate();

    /// <summary>
    ///  User setting requires that we repaint.
    /// </summary>
    private void OnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) => Invalidate();

    /// <summary>
    ///  Inheriting classes should override this method to handle this event.
    ///  Call super.onDragEnter to send this event to any registered event listeners.
    /// </summary>
    protected override void OnDragEnter(DragEventArgs devent)
    {
        base.OnDragEnter(devent);
        _dragHandler?.OleDragEnter(devent);
    }

    /// <summary>
    ///  Inheriting classes should override this method to handle this event.
    ///  Call super.onDragOver to send this event to any registered event listeners.
    /// </summary>
    protected override void OnDragOver(DragEventArgs devent)
    {
        base.OnDragOver(devent);
        _dragHandler?.OleDragOver(devent);
    }

    /// <summary>
    ///  Inheriting classes should override this method to handle this event.
    ///  Call super.onDragLeave to send this event to any registered event listeners.
    /// </summary>
    protected override void OnDragLeave(EventArgs e)
    {
        base.OnDragLeave(e);
        _dragHandler?.OleDragLeave();
    }

    /// <summary>
    ///  Inheriting classes should override this method to handle this event.
    ///  Call super.onDragDrop to send this event to any registered event listeners.
    /// </summary>
    protected override void OnDragDrop(DragEventArgs devent)
    {
        base.OnDragDrop(devent);
        _dragHandler?.OleDragDrop(devent);
    }

    /// <summary>
    ///  Inheriting classes should override this method to handle this event.
    ///  Call base.OnDoubleClick to send this event to any registered event listeners.
    /// </summary>
    protected override void OnDoubleClick(EventArgs devent)
    {
        base.OnDoubleClick(devent);
        if (_selSvc is not null)
        {
            object? selComp = _selSvc.PrimarySelection;
            Debug.Assert(selComp is not null, "Illegal selection on double-click");
            if (selComp is not null)
            {
                ISelectionUIHandler? handler = GetHandler(selComp);
                handler?.OnSelectionDoubleClick((IComponent)selComp);
            }
        }
    }

    /// <summary>
    ///  Overrides Control to handle our selection grab handles.
    /// </summary>
    // Standard 'catch all - rethrow critical' exception pattern
    protected override void OnMouseDown(MouseEventArgs me)
    {
        if (_dragHandler is null && _selSvc is not null)
        {
            try
            {
                // First, did the user step on anything?
                Point anchor = PointToScreen(new Point(me.X, me.Y));
                HitTestInfo hti = GetHitTest(anchor, HITTEST_DEFAULT);
                int hitTest = hti.hitTest;
                if ((hitTest & SelectionUIItem.CONTAINER_SELECTOR) != 0)
                {
                    _selSvc.SetSelectedComponents(new object[] { hti.selectionUIHit!._component }, SelectionTypes.Auto);
                    // Then do a drag...
                    SelectionRules rules = SelectionRules.Moveable;
                    if (((ISelectionUIService)this).BeginDrag(rules, anchor.X, anchor.Y))
                    {
                        Visible = false;
                        _containerDrag = hti.selectionUIHit._component;
                        BeginMouseDrag(anchor, hitTest);
                    }
                }
                else if (hitTest != SelectionUIItem.NOHIT && me.Button == MouseButtons.Left)
                {
                    SelectionRules rules = SelectionRules.None;
                    // If the CTRL key isn't down, select this component, otherwise, we wait until the mouse up. Make sure the component is selected
                    _ctrlSelect = (ModifierKeys & Keys.Control) != Keys.None;
                    if (!_ctrlSelect)
                    {
                        _selSvc.SetSelectedComponents(new object[] { hti.selectionUIHit!._component }, SelectionTypes.Primary);
                    }

                    if ((hitTest & SelectionUIItem.MOVE_MASK) != 0)
                    {
                        rules |= SelectionRules.Moveable;
                    }

                    if ((hitTest & SelectionUIItem.SIZE_MASK) != 0)
                    {
                        if ((hitTest & (SelectionUIItem.SIZE_X | SelectionUIItem.POS_RIGHT)) == (SelectionUIItem.SIZE_X | SelectionUIItem.POS_RIGHT))
                        {
                            rules |= SelectionRules.RightSizeable;
                        }

                        if ((hitTest & (SelectionUIItem.SIZE_X | SelectionUIItem.POS_LEFT)) == (SelectionUIItem.SIZE_X | SelectionUIItem.POS_LEFT))
                        {
                            rules |= SelectionRules.LeftSizeable;
                        }

                        if ((hitTest & (SelectionUIItem.SIZE_Y | SelectionUIItem.POS_TOP)) == (SelectionUIItem.SIZE_Y | SelectionUIItem.POS_TOP))
                        {
                            rules |= SelectionRules.TopSizeable;
                        }

                        if ((hitTest & (SelectionUIItem.SIZE_Y | SelectionUIItem.POS_BOTTOM)) == (SelectionUIItem.SIZE_Y | SelectionUIItem.POS_BOTTOM))
                        {
                            rules |= SelectionRules.BottomSizeable;
                        }

                        if (((ISelectionUIService)this).BeginDrag(rules, anchor.X, anchor.Y))
                        {
                            BeginMouseDrag(anchor, hitTest);
                        }
                    }
                    else
                    {
                        // Our mouse is in drag mode. We defer the actual move until the user moves the mouse.
                        _dragRules = rules;
                        BeginMouseDrag(anchor, hitTest);
                    }
                }
                else if (hitTest == SelectionUIItem.NOHIT)
                {
                    _dragRules = SelectionRules.None;
                    _mouseDragAnchor = s_invalidPoint;
                    return;
                }
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                if (e != CheckoutException.Canceled)
                {
                    DisplayError(e);
                }
            }
        }
    }

    /// <summary>
    ///  Overrides Control to handle our selection grab handles.
    /// </summary>
    protected override void OnMouseMove(MouseEventArgs me)
    {
        base.OnMouseMove(me);
        Point screenCoord = PointToScreen(new Point(me.X, me.Y));
        HitTestInfo hti = GetHitTest(screenCoord, HITTEST_CONTAINER_SELECTOR);
        int hitTest = hti.hitTest;
        if (hitTest != SelectionUIItem.CONTAINER_SELECTOR && hti.selectionUIHit is not null)
        {
            OnContainerSelectorActive(new ContainerSelectorActiveEventArgs());
        }

        if (_lastMoveScreenCoord == screenCoord)
        {
            return;
        }

        // If we're not dragging then set the cursor correctly.
        if (!_mouseDragging)
        {
            SetSelectionCursor(screenCoord);
        }
        else
        {
            // we have to make sure the mouse moved farther than the minimum drag distance before we actually start the drag
            if (!((ISelectionUIService)this).Dragging && (_mouseDragHitTest & SelectionUIItem.MOVE_MASK) != 0)
            {
                Size minDragSize = SystemInformation.DragSize;
                if (
                   Math.Abs(screenCoord.X - _mouseDragAnchor.X) < minDragSize.Width &&
                   Math.Abs(screenCoord.Y - _mouseDragAnchor.Y) < minDragSize.Height)
                {
                    return;
                }
                else
                {
                    _ignoreCaptureChanged = true;
                    if (((ISelectionUIService)this).BeginDrag(_dragRules, _mouseDragAnchor.X, _mouseDragAnchor.Y))
                    {
                        // we're moving, so we don't care about the ctrl key any more
                        _ctrlSelect = false;
                    }
                    else
                    {
                        EndMouseDrag(MousePosition);
                        return;
                    }
                }
            }

            Rectangle old = _mouseDragOffset;
            if ((_mouseDragHitTest & SelectionUIItem.MOVE_X) != 0)
            {
                _mouseDragOffset.X = screenCoord.X - _mouseDragAnchor.X;
            }

            if ((_mouseDragHitTest & SelectionUIItem.MOVE_Y) != 0)
            {
                _mouseDragOffset.Y = screenCoord.Y - _mouseDragAnchor.Y;
            }

            if ((_mouseDragHitTest & SelectionUIItem.SIZE_X) != 0)
            {
                if ((_mouseDragHitTest & SelectionUIItem.POS_LEFT) != 0)
                {
                    _mouseDragOffset.X = screenCoord.X - _mouseDragAnchor.X;
                    _mouseDragOffset.Width = _mouseDragAnchor.X - screenCoord.X;
                }
                else
                {
                    _mouseDragOffset.Width = screenCoord.X - _mouseDragAnchor.X;
                }
            }

            if ((_mouseDragHitTest & SelectionUIItem.SIZE_Y) != 0)
            {
                if ((_mouseDragHitTest & SelectionUIItem.POS_TOP) != 0)
                {
                    _mouseDragOffset.Y = screenCoord.Y - _mouseDragAnchor.Y;
                    _mouseDragOffset.Height = _mouseDragAnchor.Y - screenCoord.Y;
                }
                else
                {
                    _mouseDragOffset.Height = screenCoord.Y - _mouseDragAnchor.Y;
                }
            }

            if (!old.Equals(_mouseDragOffset))
            {
                Rectangle delta = _mouseDragOffset;
                delta.X -= old.X;
                delta.Y -= old.Y;
                delta.Width -= old.Width;
                delta.Height -= old.Height;
                if (delta.X != 0 || delta.Y != 0 || delta.Width != 0 || delta.Height != 0)
                {
                    // Go to default cursor for moves...
                    if ((_mouseDragHitTest & SelectionUIItem.MOVE_X) != 0 || (_mouseDragHitTest & SelectionUIItem.MOVE_Y) != 0)
                    {
                        Cursor = Cursors.Default;
                    }

                    ((ISelectionUIService)this).DragMoved(delta);
                }
            }
        }
    }

    /// <summary>
    ///  Overrides Control to handle our selection grab handles.
    /// </summary>
    // Standard 'catch all - rethrow critical' exception pattern
    protected override void OnMouseUp(MouseEventArgs me)
    {
        try
        {
            Point screenCoord = PointToScreen(new Point(me.X, me.Y));
            if (_ctrlSelect && !_mouseDragging && _selSvc is not null)
            {
                HitTestInfo hti = GetHitTest(screenCoord, HITTEST_DEFAULT);
                _selSvc.SetSelectedComponents(new object[] { hti.selectionUIHit!._component }, SelectionTypes.Primary);
            }

            if (_mouseDragging)
            {
                object? oldContainerDrag = _containerDrag;
                bool oldDragMoved = _dragMoved;
                EndMouseDrag(screenCoord);
                if (((ISelectionUIService)this).Dragging)
                {
                    ((ISelectionUIService)this).EndDrag(false);
                }

                if (me.Button == MouseButtons.Right && oldContainerDrag is not null && !oldDragMoved)
                {
                    OnContainerSelectorActive(new ContainerSelectorActiveEventArgs());
                }
            }
        }
        catch (Exception e) when (!e.IsCriticalException())
        {
            if (e != CheckoutException.Canceled)
            {
                DisplayError(e);
            }
        }
    }

    /// <summary>
    ///  If the selection manager move, this indicates that the form has autoscrolling enabled and has been scrolled.
    ///  We have to invalidate here because we may get moved before the rest of the components so we may draw the
    ///  selection in the wrong spot.
    /// </summary>
    protected override void OnMove(EventArgs e)
    {
        base.OnMove(e);
        Invalidate();
    }

    /// <summary>
    ///  overrides control.onPaint. here we paint the selection handles. The window's region was setup earlier.
    /// </summary>
    protected override void OnPaint(PaintEventArgs e)
    {
        // Paint the regular selection items first, and then the container selectors last so they draw over the top.
        foreach (SelectionUIItem item in _selectionItems.Values)
        {
            if (item is ContainerSelectionUIItem)
            {
                continue;
            }

            item.DoPaint(e.Graphics);
        }

        foreach (SelectionUIItem item in _selectionItems.Values)
        {
            if (item is ContainerSelectionUIItem)
            {
                item.DoPaint(e.Graphics);
            }
        }
    }

    /// <summary>
    ///  Sets the appropriate selection cursor at the given point.
    /// </summary>
    private void SetSelectionCursor(Point pt)
    {
        Point clientCoords = PointToClient(pt);
        // We render the cursor in the same order we paint.
        foreach (SelectionUIItem item in _selectionItems.Values)
        {
            if (item is ContainerSelectionUIItem)
            {
                continue;
            }

            Cursor? cursor = item.GetCursorAtPoint(clientCoords);
            if (cursor is not null)
            {
                Cursor = cursor == Cursors.Default ? null : cursor;
                return;
            }
        }

        foreach (SelectionUIItem item in _selectionItems.Values)
        {
            if (item is ContainerSelectionUIItem)
            {
                Cursor? cursor = item.GetCursorAtPoint(clientCoords);
                if (cursor is not null)
                {
                    Cursor = cursor == Cursors.Default ? null : cursor;
                    return;
                }
            }
        }

        // Don't know what to set; just use the default.
        Cursor = null;
    }

    /// <summary>
    ///  called when the overlay region is invalid and should be updated
    /// </summary>
    private void UpdateWindowRegion()
    {
        Region region = new(new Rectangle(0, 0, 0, 0));
        foreach (SelectionUIItem item in _selectionItems.Values)
        {
            region.Union(item.GetRegion());
        }

        Region = region;
    }

    /// <summary>
    ///  Override of our control's WNDPROC. We diddle with capture a bit, and it's important to turn this off if the capture changes.
    /// </summary>
    protected override void WndProc(ref Message m)
    {
        switch (m.MsgInternal)
        {
            case PInvokeCore.WM_LBUTTONUP:
            case PInvokeCore.WM_RBUTTONUP:
                if (_mouseDragAnchor != s_invalidPoint)
                {
                    _ignoreCaptureChanged = true;
                }

                break;
            case PInvokeCore.WM_CAPTURECHANGED:
                if (!_ignoreCaptureChanged && _mouseDragAnchor != s_invalidPoint)
                {
                    EndMouseDrag(MousePosition);
                    if (((ISelectionUIService)this).Dragging)
                    {
                        ((ISelectionUIService)this).EndDrag(true);
                    }
                }

                _ignoreCaptureChanged = false;
                break;
        }

        base.WndProc(ref m);
    }

    /// <summary>
    ///  This can be used to determine if the user is in the middle of a drag operation.
    /// </summary>
    bool ISelectionUIService.Dragging
    {
        get => _dragHandler is not null;
    }

    /// <summary>
    ///  Determines if the selection UI is shown or not.
    /// </summary>
    bool ISelectionUIService.Visible
    {
        get => Visible;
        set => Visible = value;
    }

    /// <summary>
    ///  Adds an event handler to the ContainerSelectorActive event.
    ///  This event is fired whenever the user interacts with the container selector in a manor that
    ///  would indicate that the selector should continued to be displayed.
    ///  Since the container selector normally will vanish after a timeout,
    ///  designers should listen to this event and reset the timeout when this event occurs.
    /// </summary>
    event ContainerSelectorActiveEventHandler? ISelectionUIService.ContainerSelectorActive
    {
        add => _containerSelectorActive += value;
        remove => _containerSelectorActive -= value;
    }

    /// <summary>
    ///  Assigns a selection UI handler to a given component.
    ///  The handler will be called when the UI service needs information about the component.
    ///  A single selection UI handler can be assigned to multiple components.
    ///  When multiple components are dragged, only a single handler may control the drag.
    ///  Because of this, only components that are assigned the same handler as the primary selection
    ///  are included in drag operations. A selection UI handler is automatically unassigned when the component
    ///  is removed from the container or disposed.
    /// </summary>
    void ISelectionUIService.AssignSelectionUIHandler(object component, ISelectionUIHandler handler)
    {
        if (_selectionHandlers.TryGetValue(component, out ISelectionUIHandler? oldHandler))
        {
            // The collection editors do not dispose objects from the collection before setting a new collection.
            // This causes items that are common to the old and new collections to come through this code path again,
            // causing the exception to fire. So, we check to see if the SelectionUIHandler is same,
            // and bail out in that case.
            if (handler == oldHandler)
            {
                return;
            }

            Debug.Fail("A component may have only one selection UI handler.");
            throw new InvalidOperationException();
        }

        _selectionHandlers[component] = handler;
        // If this component is selected, create a new UI handler for it.
        if (_selSvc is not null && _selSvc.GetComponentSelected(component))
        {
            SelectionUIItem item = new(this, component);
            _selectionItems[component] = item;
            UpdateWindowRegion();
            item.Invalidate();
        }
    }

    void ISelectionUIService.ClearSelectionUIHandler(object component, ISelectionUIHandler handler)
    {
        _selectionHandlers.TryGetValue(component, out ISelectionUIHandler? oldHandler);
        if (oldHandler == handler)
        {
            _selectionHandlers.Remove(component);
        }
    }

    /// <summary>
    ///  This can be called by an outside party to begin a drag of the currently selected set of components.
    /// </summary>
    // Standard 'catch all - rethrow critical' exception pattern
    bool ISelectionUIService.BeginDrag(SelectionRules rules, int initialX, int initialY)
    {
        if (_dragHandler is not null)
        {
            Debug.Fail("Caller is starting a drag, but there is already one in progress -- we cannot nest these!");
            return false;
        }

        if (rules == SelectionRules.None)
        {
            Debug.Fail("Caller is starting requesting a drag with no drag rules.");
            return false;
        }

        if (_selSvc is null)
        {
            return false;
        }

        _savedVisible = Visible;
        // First, get the list of controls
        ICollection col = _selSvc.GetSelectedComponents();
        object[] objects = new object[col.Count];
        col.CopyTo(objects, 0);
        objects = ((ISelectionUIService)this).FilterSelection(objects, rules);
        if (objects.Length == 0)
        {
            return false; // nothing selected
        }

        // We allow all components with the same UI handler as the primary selection to participate in the drag.
        ISelectionUIHandler? primaryHandler = null;
        object? primary = _selSvc.PrimarySelection;
        if (primary is not null)
        {
            primaryHandler = GetHandler(primary);
        }

        if (primaryHandler is null)
        {
            return false; // no UI handler for selection
        }

        // Now within the given selection, add those items that have the same UI handler and that have the proper rule constraints.
        List<object> list = [];
        for (int i = 0; i < objects.Length; i++)
        {
            if (GetHandler(objects[i]) == primaryHandler)
            {
                SelectionRules compRules = primaryHandler.GetComponentRules(objects[i]);
                if ((compRules & rules) == rules)
                {
                    list.Add(objects[i]);
                }
            }
        }

        if (list.Count == 0)
        {
            return false; // nothing matching the given constraints
        }

        objects = [.. list];
        bool dragging = false;
        // We must setup state before calling QueryBeginDrag. It is possible that QueryBeginDrag will cancel a drag
        // (if it places a modal dialog, for example), so we must have the drag data all setup before it cancels.
        // Then, we will check again after QueryBeginDrag to see if a cancel happened.
        _dragComponents = objects;
        _dragRules = rules;
        _dragHandler = primaryHandler;
        string transactionName = GetTransactionName(rules, objects);
        _dragTransaction = _host.CreateTransaction(transactionName);
        try
        {
            if (primaryHandler.QueryBeginDrag(objects, rules, initialX, initialY))
            {
                if (_dragHandler is not null)
                {
                    try
                    {
                        dragging = primaryHandler.BeginDrag(objects, rules, initialX, initialY);
                    }
                    catch (Exception e)
                    {
                        Debug.Fail("Drag handler threw during BeginDrag -- bad handler!", e.ToString());
                        dragging = false;
                    }
                }
            }
        }
        finally
        {
            if (!dragging)
            {
                _dragComponents = null;
                _dragRules = 0;
                _dragHandler = null;

                // Always commit this -- BeginDrag returns false for our drags because it is a complete operation.
                _dragTransaction?.Commit();
                _dragTransaction = null;
            }
        }

        return dragging;
    }

    /// <summary>
    ///  Called by an outside party to update drag information. This can only be called after a successful call to beginDrag.
    /// </summary>
    void ISelectionUIService.DragMoved(Rectangle offset)
    {
        Rectangle newOffset = Rectangle.Empty;
        if (_dragHandler is null)
        {
            throw new InvalidOperationException(SR.DesignerBeginDragNotCalled);
        }

        Debug.Assert(_dragComponents is not null, "We should have a set of drag controls here");
        if ((_dragRules & SelectionRules.Moveable) == SelectionRules.None && (_dragRules & (SelectionRules.TopSizeable | SelectionRules.LeftSizeable)) == SelectionRules.None)
        {
            newOffset = offset with { X = 0, Y = 0 };
        }

        if ((_dragRules & SelectionRules.AllSizeable) == SelectionRules.None)
        {
            if (newOffset.IsEmpty)
            {
                newOffset = offset with { Width = 0, Height = 0 };
            }
            else
            {
                newOffset.Width = newOffset.Height = 0;
            }
        }

        if (!newOffset.IsEmpty)
        {
            offset = newOffset;
        }

        Visible = false;
        _dragMoved = true;
        _dragHandler.DragMoved(_dragComponents, offset);
    }

    /// <summary>
    ///  Called by an outside party to finish a drag operation. This can only be called after a successful call to beginDrag.
    /// </summary>
    // Standard 'catch all - rethrow critical' exception pattern
    void ISelectionUIService.EndDrag(bool cancel)
    {
        _containerDrag = null;
        ISelectionUIHandler? handler = _dragHandler;
        object[] components = _dragComponents!;
        // Clean these out so that even if we throw an exception we don't die.
        _dragHandler = null;
        _dragComponents = null;
        _dragRules = SelectionRules.None;
        if (handler is null)
        {
            throw new InvalidOperationException();
        }

        // Typically, the handler will be changing a bunch of component properties here.
        // Optimize this by enclosing it within a batch call.
        DesignerTransaction? trans = null;
        try
        {
            IComponent? comp = components[0] as IComponent;
            if (components.Length > 1 || (components.Length == 1 && comp is not null && comp.Site is null))
            {
                trans = _host.CreateTransaction(string.Format(SR.DragDropMoveComponents, components.Length));
            }
            else if (components.Length == 1)
            {
                if (comp is not null)
                {
                    trans = _host.CreateTransaction(string.Format(SR.DragDropMoveComponent, comp.Site!.Name));
                }
            }

            try
            {
                handler.EndDrag(components, cancel);
            }
            catch (Exception e)
            {
                Debug.Fail(e.ToString());
            }
        }
        finally
        {
            trans?.Commit();

            // Reset the selection. This will re-display our selection.
            Visible = _savedVisible;
            ((ISelectionUIService)this).SyncSelection();
            _dragTransaction?.Commit();
            _dragTransaction = null;

            // In case this drag was initiated by us, ensure that our mouse state is correct
            EndMouseDrag(MousePosition);
        }
    }

    /// <summary>
    ///  Filters the set of selected components. The selection service will retrieve all components that are currently selected.
    ///  This method allows you to filter this set down to components that match your criteria.
    ///  The selectionRules parameter must contain one or more flags from the SelectionRules class.
    ///  These flags allow you to constrain the set of selected objects to visible, movable, sizeable or all objects.
    /// </summary>
    object[] ISelectionUIService.FilterSelection(object[] components, SelectionRules selectionRules)
    {
        object[]? selection = null;
        if (components is null)
        {
            return [];
        }

        // Mask off any selection object that doesn't adhere to the given ruleset.
        // We can ignore this if the ruleset is zero, as all components would be accepted.
        if (selectionRules != SelectionRules.None)
        {
            List<object> list = [];
            foreach (object comp in components)
            {
                if (_selectionItems.TryGetValue(comp, out SelectionUIItem? item) && item is not ContainerSelectionUIItem)
                {
                    if ((item.GetRules() & selectionRules) == selectionRules)
                    {
                        list.Add(comp);
                    }
                }
            }

            selection = [.. list];
        }

        return selection ?? [];
    }

    /// <summary>
    ///  Retrieves the width and height of a selection border grab handle.
    ///  Designers may need this to properly position their user interfaces.
    /// </summary>
    Size ISelectionUIService.GetAdornmentDimensions(AdornmentType adornmentType) => adornmentType switch
    {
        AdornmentType.GrabHandle => new Size(SelectionUIItem.GRABHANDLE_WIDTH, SelectionUIItem.GRABHANDLE_HEIGHT),
        AdornmentType.ContainerSelector or AdornmentType.Maximum => new Size(ContainerSelectionUIItem.CONTAINER_WIDTH, ContainerSelectionUIItem.CONTAINER_HEIGHT),
        _ => new Size(0, 0),
    };

    /// <summary>
    ///  Tests to determine if the given screen coordinate is over an adornment for the specified component.
    ///  This will only return true if the adornment, and selection UI, is visible.
    /// </summary>
    bool ISelectionUIService.GetAdornmentHitTest(object component, Point value) =>
        GetHitTest(value, HITTEST_DEFAULT).hitTest != SelectionUIItem.NOHIT;

    /// <summary>
    ///  Determines if the component is currently "container" selected. Container selection is a visual aid for
    ///  selecting containers. It doesn't affect the normal "component" selection.
    /// </summary>
    bool ISelectionUIService.GetContainerSelected([NotNullWhen(true)] object? component)
        => (component is not null
        && _selectionItems.TryGetValue(component, out SelectionUIItem? value)
        && value is ContainerSelectionUIItem);

    /// <summary>
    ///  Retrieves a set of flags that define rules for the selection.
    ///  Selection rules indicate if the given component can be moved or sized, for example.
    /// </summary>
    SelectionRules ISelectionUIService.GetSelectionRules(object component)
    {
        if (!_selectionItems.TryGetValue(component, out SelectionUIItem? selection))
        {
            Debug.Fail("The component is not currently selected.");
            throw new InvalidOperationException();
        }

        return selection.GetRules();
    }

    /// <summary>
    ///  Allows you to configure the style of the selection frame that a component uses.
    ///  This is useful if your component supports different modes of operation
    ///  (such as an in-place editing mode and a static design mode).
    ///  Where possible, you should leave the selection style as is and use the design-time hit testing
    ///  feature of the IDesigner interface to provide features at design time.
    ///  The value of style must be one of the SelectionStyle enum values.
    ///  The selection style is only valid for the duration that the component is selected.
    /// </summary>
    SelectionStyles ISelectionUIService.GetSelectionStyle(object component)
        => !_selectionItems.TryGetValue(component, out SelectionUIItem? item) ? SelectionStyles.None : item.Style;

    /// <summary>
    ///  Changes the container selection status of the given component.
    ///  Container selection is a visual aid for selecting containers.
    ///  It doesn't affect the normal "component" selection.
    /// </summary>
    void ISelectionUIService.SetContainerSelected(object component, bool selected)
    {
        if (selected)
        {
            _selectionItems.TryGetValue(component, out SelectionUIItem? existingItem);
            if (existingItem is not ContainerSelectionUIItem)
            {
                existingItem?.Dispose();

                SelectionUIItem item = new ContainerSelectionUIItem(this, component);
                _selectionItems[component] = item;
                // Now update our region and invalidate
                UpdateWindowRegion();
                existingItem?.Invalidate();

                item.Invalidate();
            }
        }
        else
        {
            if (!_selectionItems.TryGetValue(component, out SelectionUIItem? existingItem) || existingItem is ContainerSelectionUIItem)
            {
                _selectionItems.Remove(component);
                existingItem?.Dispose();

                UpdateWindowRegion();
                existingItem?.Invalidate();
            }
        }
    }

    /// <summary>
    ///  Allows you to configure the style of the selection frame that a component uses.
    ///  This is useful if your component supports different modes of operation
    ///  (such as an in-place editing mode and a static design mode). Where possible,
    ///  you should leave the selection style as is and use the design-time hit testing feature of the
    ///  <see cref="IDesigner"/> interface to provide features at design time.
    ///  The value of style must be one of the SelectionStyle enum values.
    ///  The selection style is only valid for the duration that the component is selected.
    /// </summary>
    void ISelectionUIService.SetSelectionStyle(object component, SelectionStyles style)
    {
        _selectionItems.TryGetValue(component, out SelectionUIItem? selUI);
        if (_selSvc is not null && _selSvc.GetComponentSelected(component))
        {
            selUI = new SelectionUIItem(this, component);
            _selectionItems[component] = selUI;
        }

        if (selUI is not null)
        {
            selUI.Style = style;
            UpdateWindowRegion();
            selUI.Invalidate();
        }
    }

    /// <summary>
    ///  This should be called when a component has been moved, sized or re-parented, but the change was not the result
    ///  of a property change. All property changes are monitored by the selection UI service, so this is automatic
    ///  most of the time. There are times, however, when a component may be moved without a property change
    ///  notification occurring. Scrolling an auto scroll Win32 form is an example of this.
    ///  This method simply re-queries all currently selected components for their bounds and
    ///  updates the selection handles for any that have changed.
    /// </summary>
    void ISelectionUIService.SyncSelection()
    {
        if (_batchMode)
        {
            _batchChanged = true;
        }
        else
        {
            if (IsHandleCreated)
            {
                bool updateRegion = false;
                foreach (SelectionUIItem item in _selectionItems.Values)
                {
                    updateRegion |= item.UpdateSize();
                    item.UpdateRules();
                }

                if (updateRegion)
                {
                    UpdateWindowRegion();
                    Update();
                }
            }
        }
    }

    /// <summary>
    ///  This should be called when a component's property changed, that the designer thinks should result
    ///  in a selection UI change. This method simply re-queries all currently selected components for their bounds
    ///  and updates the selection handles for any that have changed.
    /// </summary>
    void ISelectionUIService.SyncComponent(object? component)
    {
        if (_batchMode)
        {
            _batchSync = true;
        }
        else
        {
            if (IsHandleCreated)
            {
                foreach (SelectionUIItem item in _selectionItems.Values)
                {
                    item.UpdateRules();
                    item.Dispose();
                }

                UpdateWindowRegion();
                Invalidate();
                Update();
            }
        }
    }
}
