// 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 System.Windows.Forms.Design.Behavior;

namespace System.Windows.Forms.Design;

/// <summary>
///  This class implements our design time document. This is the outer window that encompasses a designer.
///  It maintains a control hierarchy that looks like this:
///
///  - DesignerFrame
///  - ScrollableControl
///  - Designer
///  - Splitter
///  - ScrollableControl
///  - Component Tray
///
///  The splitter and second scrollable control are created on demand when a tray is added.
/// </summary>
internal class DesignerFrame : Control, IOverlayService, ISplitWindowService, IContainsThemedScrollbarWindows
{
    private readonly ISite _designerSite;
    private readonly OverlayControl _designerRegion;
    private Splitter? _splitter;
    private Control? _designer;
    private BehaviorService? _behaviorService;
    private readonly IUIService? _uiService;

    /// <summary>
    ///  Initializes a new instance of the <see cref="DesignerFrame"/> class.
    /// </summary>
    public DesignerFrame(ISite site)
    {
        Text = "DesignerFrame";
        _designerSite = site;
        _designerRegion = new OverlayControl(site);
        _uiService = _designerSite.GetService<IUIService>();
        if (_uiService?.Styles["ArtboardBackground"] is Color color)
        {
            BackColor = color;
        }

        Controls.Add(_designerRegion);
        // Now we must configure our designer to be at the correct location, and setup the autoscrolling for its container.
        _designerRegion.AutoScroll = true;
        _designerRegion.Dock = DockStyle.Fill;
    }

    /// <summary>
    ///  Returns the scroll offset for the scrollable control that manages all overlays. This is needed by the
    ///  BehaviorService so we can correctly invalidate our AdornerWindow based on scrollposition.
    /// </summary>
    internal Point AutoScrollPosition => _designerRegion.AutoScrollPosition;

    /// <summary>
    ///  Demand creates a ptr to the BehaviorService - we do this so we can route keyboard message to it.
    /// </summary>
    private BehaviorService? BehaviorService => _behaviorService ??= _designerSite.GetService<BehaviorService>();

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_designer is not null)
            {
                Control designerHolder = _designer;
                _designer = null;
                designerHolder.Visible = false;
                designerHolder.Parent = null;
            }

            _splitter?.SplitterMoved -= OnSplitterMoved;
        }

        base.Dispose(disposing);
    }

    private unsafe void ForceDesignerRedraw(bool focus)
    {
        if (_designer is not null && _designer.IsHandleCreated)
        {
            PInvokeCore.SendMessage(_designer, PInvokeCore.WM_NCACTIVATE, (WPARAM)(BOOL)focus);
            PInvoke.RedrawWindow(_designer, lprcUpdate: null, HRGN.Null, REDRAW_WINDOW_FLAGS.RDW_FRAME);
        }
    }

    /// <summary>
    ///  Initializes this frame with the given designer view.
    /// </summary>
    public void Initialize(Control view)
    {
        _designer = view;
        if (_designer is Form form)
        {
            form.TopLevel = false;
        }

        _designerRegion.Controls.Add(_designer);
        SyncDesignerUI();
        _designer.Visible = true;
        _designer.Enabled = true;
    }

    /// <summary>
    ///  When we get an lose focus, we need to make sure the form designer knows about it so it'll paint it's caption right.
    /// </summary>
    protected override void OnGotFocus(EventArgs e)
    {
        ForceDesignerRedraw(true);
        ISelectionService? selSvc = _designerSite.GetService<ISelectionService>();
        if (selSvc?.PrimarySelection is Control { IsDisposed: false } ctrl)
        {
            PInvoke.NotifyWinEvent(
                (uint)AccessibleEvents.Focus,
                ctrl,
                (int)OBJECT_IDENTIFIER.OBJID_CLIENT,
                (int)PInvoke.CHILDID_SELF);
        }
    }

    /// <summary>
    ///  When we get an lose focus, we need to make sure the form designer knows about it so it'll paint it's caption right.
    /// </summary>
    protected override void OnLostFocus(EventArgs e)
    {
        ForceDesignerRedraw(false);
    }

    private void OnSplitterMoved(object? sender, SplitterEventArgs e)
    {
        // Dirty the designer.
        if (_designerSite.TryGetService(out IComponentChangeService? changeService))
        {
            try
            {
                changeService.OnComponentChanging(_designerSite.Component);
                changeService.OnComponentChanged(_designerSite.Component);
            }
            catch (Exception ex) when (!ex.IsCriticalException())
            {
            }
        }
    }

    /// <summary>
    ///  We override this to do nothing. Otherwise, all the nice keyboard messages we want would get run through
    ///  the Form's keyboard handling procedure.
    /// </summary>
    protected override bool ProcessDialogKey(Keys keyData)
    {
        return false;
    }

    private void SyncDesignerUI()
    {
        Size selectionSize = DesignerUtils.GetAdornmentDimensions(AdornmentType.Maximum);
        _designerRegion.AutoScrollMargin = selectionSize;
        _designer!.Location = new Point(selectionSize.Width, selectionSize.Height);
        BehaviorService?.SyncSelection();
    }

    /// <summary>
    ///  Base wndProc. All messages are sent to wndProc after getting filtered through the preProcessMessage function.
    ///  Inheriting controls should call base.wndProc for any messages that they don't handle.
    /// </summary>
    protected override void WndProc(ref Message m)
    {
        switch (m.MsgInternal)
        {
            // Provide MouseWheel access for scrolling
            case PInvokeCore.WM_MOUSEWHEEL:
                // Send a message to ourselves to scroll
                if (!_designerRegion._messageMouseWheelProcessed)
                {
                    _designerRegion._messageMouseWheelProcessed = true;
                    PInvokeCore.SendMessage(_designerRegion, PInvokeCore.WM_MOUSEWHEEL, m.WParamInternal, m.LParamInternal);
                    return;
                }

                break;
            // Provide keyboard access for scrolling
            case PInvokeCore.WM_KEYDOWN:
                Keys keycode = (Keys)(m.WParamInternal & 0xFFFF);
                (SCROLLBAR_COMMAND wScrollNotify, MessageId msg) = keycode switch
                {
                    Keys.Up => (SCROLLBAR_COMMAND.SB_LINEUP, (MessageId)PInvokeCore.WM_VSCROLL),
                    Keys.Down => (SCROLLBAR_COMMAND.SB_LINEDOWN, (MessageId)PInvokeCore.WM_VSCROLL),
                    Keys.PageUp => (SCROLLBAR_COMMAND.SB_PAGEUP, (MessageId)PInvokeCore.WM_VSCROLL),
                    Keys.PageDown => (SCROLLBAR_COMMAND.SB_PAGEDOWN, (MessageId)PInvokeCore.WM_VSCROLL),
                    Keys.Home => (SCROLLBAR_COMMAND.SB_TOP, (MessageId)PInvokeCore.WM_VSCROLL),
                    Keys.End => (SCROLLBAR_COMMAND.SB_BOTTOM, (MessageId)PInvokeCore.WM_VSCROLL),
                    Keys.Left => (SCROLLBAR_COMMAND.SB_LINEUP, (MessageId)PInvokeCore.WM_HSCROLL),
                    Keys.Right => (SCROLLBAR_COMMAND.SB_LINEDOWN, (MessageId)PInvokeCore.WM_HSCROLL),
                    _ => ((SCROLLBAR_COMMAND)0, (MessageId)PInvokeCore.WM_NULL)
                };

                if (msg == PInvokeCore.WM_VSCROLL || msg == PInvokeCore.WM_HSCROLL)
                {
                    // Send a message to ourselves to scroll
                    PInvokeCore.SendMessage(_designerRegion, msg, (WPARAM)(int)wScrollNotify);
                    return;
                }

                break;
            case PInvokeCore.WM_CONTEXTMENU:
                PInvokeCore.SendMessage(_designer!, m.MsgInternal, m.WParamInternal, m.LParamInternal);
                return;
        }

        base.WndProc(ref m);
    }

    /// <summary>
    ///  Pushes the given control on top of the overlay list. This is a "push" operation, meaning that it forces
    ///  this control to the top of the existing overlay list.
    /// </summary>
    int IOverlayService.PushOverlay(Control control) => _designerRegion.PushOverlay(control);

    /// <summary>
    ///  Removes the given control from the overlay list. Unlike pushOverlay, this can remove a control from the
    ///  middle of the overlay list.
    /// </summary>
    void IOverlayService.RemoveOverlay(Control control)
    {
        _designerRegion.RemoveOverlay(control);
    }

    /// <summary>
    ///  Inserts the overlay.
    /// </summary>
    void IOverlayService.InsertOverlay(Control control, int index)
    {
        _designerRegion.InsertOverlay(control, index);
    }

    /// <summary>
    ///  Invalidate child overlays
    /// </summary>
    void IOverlayService.InvalidateOverlays(Rectangle screenRectangle)
    {
        _designerRegion.InvalidateOverlays(screenRectangle);
    }

    /// <summary>
    ///  Invalidate child overlays
    /// </summary>
    void IOverlayService.InvalidateOverlays(Region screenRegion)
    {
        _designerRegion.InvalidateOverlays(screenRegion);
    }

    /// <summary>
    ///  Requests the service to add a window 'pane'.
    /// </summary>
    void ISplitWindowService.AddSplitWindow(Control window)
    {
        if (_splitter is null)
        {
            _splitter = new Splitter();
            if (_uiService?.Styles["HorizontalResizeGrip"] is Color color)
            {
                _splitter.BackColor = color;
            }
            else
            {
                _splitter.BackColor = SystemColors.Control;
            }

            _splitter.BorderStyle = BorderStyle.Fixed3D;
            _splitter.Height = 7;
            _splitter.Dock = DockStyle.Bottom;
            _splitter.SplitterMoved += OnSplitterMoved;
        }

        SuspendLayout();
        window.Dock = DockStyle.Bottom;

        // Compute a minimum height for this window.
        int minHeight = 80;
        if (window.Height < minHeight)
        {
            window.Height = minHeight;
        }

        Controls.Add(_splitter);
        Controls.Add(window);
        ResumeLayout();
    }

    /// <summary>
    ///  Requests the service to remove a window 'pane'.
    /// </summary>
    void ISplitWindowService.RemoveSplitWindow(Control window)
    {
        SuspendLayout();
        Controls.Remove(window);
        Controls.Remove(_splitter);
        ResumeLayout();
    }

    /// <summary>
    ///  Returns IEnumerable of all windows which need to be themed when running inside VS We don't know how to do
    ///  theming here but we know which windows need to be themed. The two ScrollableControls that hold the designer
    ///  and the tray need to be themed, all of the children of the designed form should not be themed. The tray
    ///  contains only controls which are not visible in the user app but are visible inside VS. As a result, we
    ///  want to theme all windows within the tray but only the top window for the designer pane.
    /// </summary>
    IEnumerable IContainsThemedScrollbarWindows.ThemedScrollbarWindows()
    {
        List<ThemedScrollbarWindow> windows = new(Controls.Count);
        foreach (Control c in Controls)
        {
            ThemedScrollbarWindow windowInfo = new() { Handle = c.Handle };
            if (c is OverlayControl)
            {
                windowInfo.Mode = ThemedScrollbarMode.OnlyTopLevel;
            }
            else
            {
                windowInfo.Mode = ThemedScrollbarMode.All;
            }

            windows.Add(windowInfo);
        }

        return windows;
    }

    /// <summary>
    ///  This is a scrollable control that supports additional floating overlay controls.
    /// </summary>
    private class OverlayControl : ScrollableControl
    {
        private readonly List<Control> _overlayList;
        private readonly IServiceProvider _provider;
        internal bool _messageMouseWheelProcessed;
        private BehaviorService? _behaviorService;

        /// <summary>
        ///  Creates a new overlay control.
        /// </summary>
        public OverlayControl(IServiceProvider provider)
        {
            _provider = provider;
            _overlayList = [];
            AutoScroll = true;
            Text = "OverlayControl";
        }

        protected override AccessibleObject CreateAccessibilityInstance()
        {
            return new OverlayControlAccessibleObject(this);
        }

        /// <summary>
        ///  Demand creates a ptr to the BehaviorService
        /// </summary>
        private BehaviorService? BehaviorService => _behaviorService ??= _provider.GetService<BehaviorService>();

        /// <summary>
        ///  At handle creation time we request the designer's handle and parent it.
        /// </summary>
        protected override void OnCreateControl()
        {
            base.OnCreateControl();
            // Loop through all of the overlays, create them, and hook them up
            foreach (Control c in _overlayList)
            {
                ParentOverlay(c);
            }

            // We've reparented everything, which means that our selection UI is probably out of sync. Ask it to sync.
            BehaviorService?.SyncSelection();
        }

        /// <summary>
        ///  We override onLayout to provide our own custom layout functionality. This just overlaps all of the controls.
        /// </summary>
        protected override void OnLayout(LayoutEventArgs e)
        {
            base.OnLayout(e);
            Rectangle client = DisplayRectangle;

            // Loop through all of the overlays and size them. Also make sure that they are still on top of the
            // Z-order, because a handle recreate could have changed this.
            foreach (Control c in _overlayList)
            {
                c.Bounds = client;
            }
        }

        /// <summary>
        ///  Called to parent an overlay window into our document. This assumes that we call in reverse stack
        ///  order, as it always pushes to the top of the z-order.
        /// </summary>
        private void ParentOverlay(Control control)
        {
            PInvoke.SetParent(control, this);
            PInvoke.SetWindowPos(
                control,
                HWND.HWND_TOP,
                0, 0, 0, 0,
                SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE);
        }

        /// <summary>
        ///  Pushes the given control on top of the overlay list. This is a "push" operation, meaning that it
        ///  forces this control to the top of the existing overlay list.
        /// </summary>
        public int PushOverlay(Control control)
        {
            Debug.Assert(_overlayList.IndexOf(control) == -1, $"Duplicate overlay in overlay service: {control.GetType().FullName}");
            _overlayList.Add(control);
            // We need to have these components parented, but we don't want them to effect our layout.
            if (IsHandleCreated)
            {
                ParentOverlay(control);
                control.Bounds = DisplayRectangle;
            }

            return _overlayList.IndexOf(control);
        }

        /// <summary>
        ///  Removes the given control from the overlay list. Unlike pushOverlay, this can remove a control from
        ///  the middle of the overlay list.
        /// </summary>
        public void RemoveOverlay(Control control)
        {
            Debug.Assert(_overlayList.IndexOf(control) != -1, $"Control is not in overlay service: {control.GetType().FullName}");
            _overlayList.Remove(control);
            control.Visible = false;
            control.Parent = null;
        }

        /// <summary>
        ///  Inserts Overlay.
        /// </summary>
        public void InsertOverlay(Control control, int index)
        {
            Debug.Assert(_overlayList.IndexOf(control) == -1, $"Duplicate overlay in overlay service: {control.GetType().FullName}");
            Control c = _overlayList[index];
            RemoveOverlay(c);
            PushOverlay(control);
            PushOverlay(c);
            c.Visible = true;
        }

        /// <summary>
        ///  Invalidates overlays that intersect with the given section of the screen;
        /// </summary>
        public void InvalidateOverlays(Rectangle screenRectangle)
        {
            // paint in inverse order so that things at the front paint last.
            for (int i = _overlayList.Count - 1; i >= 0; i--)
            {
                Control overlayControl = _overlayList[i];
                Rectangle invalidateRect = new(overlayControl.PointToClient(screenRectangle.Location), screenRectangle.Size);
                if (overlayControl.ClientRectangle.IntersectsWith(invalidateRect))
                {
                    overlayControl.Invalidate(invalidateRect);
                }
            }
        }

        /// <summary>
        ///  Invalidates overlays that intersect with the given section of the screen;
        /// </summary>
        public void InvalidateOverlays(Region screenRegion)
        {
            // paint in inverse order so that things at the front paint last.
            for (int i = _overlayList.Count - 1; i >= 0; i--)
            {
                Control overlayControl = _overlayList[i];
                Rectangle overlayControlScreenBounds = overlayControl.Bounds;
                overlayControlScreenBounds.Location = overlayControl.PointToScreen(overlayControl.Location);
                using Region intersectionRegion = screenRegion.Clone();
                // get the intersection of everything on the screen that's invalidating and the overlaycontrol
                intersectionRegion.Intersect(overlayControlScreenBounds);
                // translate this down to overlay control coordinates.
                intersectionRegion.Translate(-overlayControlScreenBounds.X, -overlayControlScreenBounds.Y);
                overlayControl.Invalidate(intersectionRegion);
            }
        }

        /// <summary>
        ///  Need to know when child windows are created so we can properly set the Z-order
        /// </summary>
        protected override void WndProc(ref Message m)
        {
            base.WndProc(ref m);
            if (m.MsgInternal == PInvokeCore.WM_PARENTNOTIFY && m.WParamInternal.LOWORD == PInvokeCore.WM_CREATE)
            {
                bool ourWindow = false;
                foreach (Control c in _overlayList)
                {
                    if (c.IsHandleCreated && m.LParamInternal == c.Handle)
                    {
                        ourWindow = true;
                        break;
                    }
                }

                if (!ourWindow)
                {
                    foreach (Control c in _overlayList)
                    {
                        PInvoke.SetWindowPos(
                            c,
                            HWND.HWND_TOP,
                            0, 0, 0, 0,
                            SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE);
                    }
                }
            }
            else if (m.Msg is (int)PInvokeCore.WM_VSCROLL or (int)PInvokeCore.WM_HSCROLL && BehaviorService is not null)
            {
                BehaviorService.SyncSelection();
            }
            else if ((m.Msg == (int)PInvokeCore.WM_MOUSEWHEEL))
            {
                _messageMouseWheelProcessed = false;
                BehaviorService?.SyncSelection();
            }
        }

        public class OverlayControlAccessibleObject : ControlAccessibleObject
        {
            public OverlayControlAccessibleObject(OverlayControl owner) : base(owner)
            {
            }

            public override AccessibleObject? HitTest(int x, int y)
            {
                // Since the SelectionUIOverlay in first in the z-order, it normally gets
                // returned from accHitTest. But we'd rather expose the form that is being
                // designed.
                //
                foreach (Control c in Owner!.Controls)
                {
                    AccessibleObject cao = c.AccessibilityObject;
                    if (cao.Bounds.Contains(x, y))
                    {
                        return cao;
                    }
                }

                return base.HitTest(x, y);
            }
        }
    }
}
