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

#nullable disable

using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;

namespace System.Windows.Forms.Design.Behavior;

/// <summary>
///  This behavior is associated with the ContainerGlyph offered up by ParentControlDesigner.  This Behavior simply  starts a new dragdrop behavior.
/// </summary>
internal sealed class ContainerSelectorBehavior : Behavior
{
    private Control _containerControl; // our related control
    private IServiceProvider _serviceProvider; // used for starting a drag/drop
    private BehaviorService _behaviorService; // ptr to where we start our drag/drop operation
    private bool _okToMove; // state identifying if we are allowed to move the container
    private Point _initialDragPoint; // cached "mouse down" point

    // For some controls, we want to change the original drag point to be the upper-left of the control in  order to make it easier to drop the control at a desired location. But not all controls want this behavior. E.g. we want to do it for Panel and ToolStrip, but not for Label. Label has a ContainerSelectorBehavior via the NoResizeSelectionBorder glyph.
    private readonly bool _setInitialDragPoint;

    /// <summary>
    ///  Constructor, here we cache off all of our member vars and sync location and size changes.
    /// </summary>
    internal ContainerSelectorBehavior(Control containerControl, IServiceProvider serviceProvider)
    {
        Init(containerControl, serviceProvider);
        _setInitialDragPoint = false;
    }

    /// <summary>
    ///  Constructor, here we cache off all of our member vars and sync location and size changes.
    /// </summary>
    internal ContainerSelectorBehavior(Control containerControl, IServiceProvider serviceProvider, bool setInitialDragPoint)
    {
        Init(containerControl, serviceProvider);
        _setInitialDragPoint = setInitialDragPoint;
    }

    private void Init(Control containerControl, IServiceProvider serviceProvider)
    {
        _behaviorService = (BehaviorService)serviceProvider.GetService(typeof(BehaviorService));
        if (_behaviorService is null)
        {
            Debug.Fail("Could not get the BehaviorService from ContainerSelectorBehavior!");
            return;
        }

        _containerControl = containerControl;
        _serviceProvider = serviceProvider;
        _initialDragPoint = Point.Empty;
        _okToMove = false;
    }

    public Control ContainerControl
    {
        get => _containerControl;
    }

    /// <summary>
    ///  This will be true when we detect a mousedown on our glyph.  The Glyph can use this state to always return 'true' from hittesting indicating that it would like all messages (like mousemove).
    /// </summary>
    public bool OkToMove
    {
        get => _okToMove;
        set => _okToMove = value;
    }

    public Point InitialDragPoint
    {
        get => _initialDragPoint;
        set => _initialDragPoint = value;
    }

    /// <summary>
    ///  If the user selects the containerglyph - select our related component.
    /// </summary>
    public override bool OnMouseDown(Glyph g, MouseButtons button, Point mouseLoc)
    {
        if (button == MouseButtons.Left)
        {
            // select our component
            ISelectionService selSvc = (ISelectionService)_serviceProvider.GetService(typeof(ISelectionService));
            if (selSvc is not null && !_containerControl.Equals(selSvc.PrimarySelection as Control))
            {
                selSvc.SetSelectedComponents(new object[] { _containerControl }, SelectionTypes.Primary | SelectionTypes.Toggle);
                // Setting the selected component will create a new glyph, so this instance of the glyph won't receive any more mouse messages. So we need to tell the new glyph what the initialDragPoint and okToMove are.
                if (g is not ContainerSelectorGlyph selOld)
                {
                    return false;
                }

                foreach (Adorner a in _behaviorService.Adorners)
                {
                    foreach (Glyph glyph in a.Glyphs)
                    {
                        if (glyph is not ContainerSelectorGlyph selNew)
                        {
                            continue;
                        }

                        // Don't care if we are looking at the same containerselectorglyph
                        if (selNew.Equals(selOld))
                        {
                            continue;
                        }

                        // Check if the containercontrols are the same
                        if (!(selNew.RelatedBehavior is ContainerSelectorBehavior behNew) || !(selOld.RelatedBehavior is ContainerSelectorBehavior behOld))
                        {
                            continue;
                        }

                        // and the related components are the same, then we have found the new glyph that just got added
                        if (behOld.ContainerControl.Equals(behNew.ContainerControl))
                        {
                            behNew.OkToMove = true;
                            behNew.InitialDragPoint = DetermineInitialDragPoint(mouseLoc);
                            break;
                        }
                    }
                }
            }
            else
            {
                InitialDragPoint = DetermineInitialDragPoint(mouseLoc);
                // set 'okToMove' to true since the user actually clicked down on the glyph
                OkToMove = true;
            }
        }

        return false;
    }

    private Point DetermineInitialDragPoint(Point mouseLoc)
    {
        if (_setInitialDragPoint)
        {
            // Set the mouse location to be to control's location.
            Point controlOrigin = _behaviorService.ControlToAdornerWindow(_containerControl);
            controlOrigin = _behaviorService.AdornerWindowPointToScreen(controlOrigin);
            Cursor.Position = controlOrigin;
            return controlOrigin;
        }
        else
        {
            // This really amounts to doing nothing
            return mouseLoc;
        }
    }

    /// <summary>
    ///  We will compare the mouse loc to the initial point (set in onmousedown) and if we're far enough, we'll create a dropsourcebehavior object and start out drag operation!
    /// </summary>
    public override bool OnMouseMove(Glyph g, MouseButtons button, Point mouseLoc)
    {
        if (button == MouseButtons.Left && OkToMove)
        {
            if (InitialDragPoint == Point.Empty)
            {
                InitialDragPoint = DetermineInitialDragPoint(mouseLoc);
            }

            Size delta = new(Math.Abs(mouseLoc.X - InitialDragPoint.X), Math.Abs(mouseLoc.Y - InitialDragPoint.Y));
            if (delta.Width >= DesignerUtils.MinDragSize.Width / 2 || delta.Height >= DesignerUtils.MinDragSize.Height / 2)
            {
                // start our drag!
                Point screenLoc = _behaviorService.AdornerWindowToScreen();
                screenLoc.Offset(mouseLoc.X, mouseLoc.Y);
                StartDragOperation(screenLoc);
            }
        }

        return false;
    }

    /// <summary>
    ///  Simply clear the initial drag point, so we can start again on the next mouse down.
    /// </summary>
    public override bool OnMouseUp(Glyph g, MouseButtons button)
    {
        InitialDragPoint = Point.Empty;
        OkToMove = false;
        return false;
    }

    /// <summary>
    ///  Called when we've identified that we want to start a drag operation with our data container.
    /// </summary>
    private void StartDragOperation(Point initialMouseLocation)
    {
        // need to grab a hold of some services
        ISelectionService selSvc = (ISelectionService)_serviceProvider.GetService(typeof(ISelectionService));
        IDesignerHost host = (IDesignerHost)_serviceProvider.GetService(typeof(IDesignerHost));
        if (selSvc is null || host is null)
        {
            Debug.Fail("Can't drag this Container! Either SelectionService is null or DesignerHost is null");
            return;
        }

        // must identify a required parent to avoid dragging mixes of children
        Control requiredParent = _containerControl.Parent;
        List<IComponent> dragControls = [];
        ICollection selComps = selSvc.GetSelectedComponents();
        // create our list of controls-to-drag
        foreach (IComponent comp in selComps)
        {
            if ((comp is Control ctrl) && (ctrl.Parent is not null))
            {
                if (!ctrl.Parent.Equals(requiredParent))
                {
                    continue; // mixed selection of different parents - don't add this
                }

                if (host.GetDesigner(ctrl) is ControlDesigner des && (des.SelectionRules & SelectionRules.Moveable) != 0)
                {
                    dragControls.Add(ctrl);
                }
            }
        }

        // if we have controls-to-drag, create our new behavior and start the drag/drop operation
        if (dragControls.Count > 0)
        {
            Point controlOrigin;
            if (_setInitialDragPoint)
            {
                // In this case we want the initialmouselocation to be the control's origin.
                controlOrigin = _behaviorService.ControlToAdornerWindow(_containerControl);
                controlOrigin = _behaviorService.AdornerWindowPointToScreen(controlOrigin);
            }
            else
            {
                controlOrigin = initialMouseLocation;
            }

            DropSourceBehavior dsb = new(dragControls, _containerControl.Parent, controlOrigin);
            try
            {
                _behaviorService.DoDragDrop(dsb);
            }
            finally
            {
                OkToMove = false;
                InitialDragPoint = Point.Empty;
            }
        }
    }
}
