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

using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Windows.Forms.Layout;
using System.Windows.Forms.Rendering.Animation;
using System.Windows.Forms.VisualStyles;
using Microsoft.Win32;

namespace System.Windows.Forms;

/// <summary>
///  Implements the basic functionality required by an up-down control.
/// </summary>
[Designer($"System.Windows.Forms.Design.UpDownBaseDesigner, {Assemblies.SystemDesign}")]
public abstract partial class UpDownBase : ContainerControl
{
    private const int DefaultWheelScrollLinesPerPage = 1;
    private const int DefaultButtonsWidth = 16;
    private const int DefaultControlWidth = 120;

    // width of custom border we draw when themed
    private const int ThemedBorderWidth = 1;

    // Modern (Net11+) chrome geometry. The edit and button group share the border thickness and
    // internal chrome inset used by TextBoxBase; only the gap between the two buttons is additional.
    private const int ModernButtonGroupSpacingLogical = 2;
    private const int ModernFocusBandHeight = 4;
    private const BorderStyle DefaultBorderStyle = BorderStyle.Fixed3D;
    private const LeftRightAlignment DefaultUpDownAlign = LeftRightAlignment.Right;
    private const int DefaultTimerInterval = 500;

    // Child controls
    internal UpDownEdit _upDownEdit;
    internal UpDownButtons _upDownButtons;

    private LeftRightAlignment _upDownAlign = DefaultUpDownAlign;

    /// <summary>
    ///  The current border for this edit control.
    /// </summary>
    private BorderStyle _borderStyle = DefaultBorderStyle;
    private AnimatedFocusIndicatorRenderer? _focusIndicatorRenderer;

    // Mouse wheel movement
    private int _wheelDelta;
    private bool _doubleClickFired;
    internal int _defaultButtonsWidth = DefaultButtonsWidth;

    /// <summary>
    ///  Initializes a new instance of the <see cref="UpDownBase"/> class.
    /// </summary>
    public UpDownBase()
    {
        _defaultButtonsWidth = LogicalToDeviceUnits(DefaultButtonsWidth);

        _upDownButtons = new UpDownButtons(this);

        _upDownEdit = new UpDownEdit(this)
        {
            BorderStyle = BorderStyle.None,
            AutoSize = false
        };

        _upDownEdit.KeyDown += OnTextBoxKeyDown;
        _upDownEdit.KeyPress += OnTextBoxKeyPress;
        _upDownEdit.TextChanged += OnTextBoxTextChanged;
        _upDownEdit.LostFocus += OnTextBoxLostFocus;
        _upDownEdit.Resize += OnTextBoxResize;
        _upDownButtons.TabStop = false;
        _upDownButtons.Size = new Size(_defaultButtonsWidth, PreferredHeight);
        _upDownButtons.UpDown += OnUpDown;

        Controls.AddRange([_upDownButtons, _upDownEdit]);

        SetStyle(ControlStyles.Opaque
            | ControlStyles.FixedHeight
            | ControlStyles.ResizeRedraw, true);

        SetStyle(ControlStyles.StandardClick, false);
        SetStyle(ControlStyles.UseTextForAccessibility, false);
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public override bool AutoScroll
    {
        get => false;
        set
        {
            // Don't allow AutoScroll to be set to anything
        }
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new Size AutoScrollMargin
    {
        get => base.AutoScrollMargin;
        set => base.AutoScrollMargin = value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new Size AutoScrollMinSize
    {
        get => base.AutoScrollMinSize;
        set => base.AutoScrollMinSize = value;
    }

    /// <summary>
    ///  Override to re-expose AutoSize.
    /// </summary>
    [Browsable(true)]
    [EditorBrowsable(EditorBrowsableState.Always)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public override bool AutoSize
    {
        get => base.AutoSize;
        set => base.AutoSize = value;
    }

    [SRCategory(nameof(SR.CatPropertyChanged))]
    [SRDescription(nameof(SR.ControlOnAutoSizeChangedDescr))]
    [Browsable(true)]
    [EditorBrowsable(EditorBrowsableState.Always)]
    public new event EventHandler? AutoSizeChanged
    {
        add => base.AutoSizeChanged += value;
        remove => base.AutoSizeChanged -= value;
    }

    /// <summary>
    ///  Gets or sets the background color for the
    ///  text box portion of the up-down control.
    /// </summary>
    public override Color BackColor
    {
        get => _upDownEdit.BackColor;
        set
        {
            base.BackColor = value; // Don't remove this or you will break serialization.
            _upDownEdit.BackColor = value;
            Invalidate();
        }
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public override Image? BackgroundImage
    {
        get => base.BackgroundImage;
        set => base.BackgroundImage = value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? BackgroundImageChanged
    {
        add => base.BackgroundImageChanged += value;
        remove => base.BackgroundImageChanged -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public override ImageLayout BackgroundImageLayout
    {
        get => base.BackgroundImageLayout;
        set => base.BackgroundImageLayout = value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? BackgroundImageLayoutChanged
    {
        add => base.BackgroundImageLayoutChanged += value;
        remove => base.BackgroundImageLayoutChanged -= value;
    }

    /// <summary>
    ///  Gets or sets the border style for the up-down control.
    /// </summary>
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(BorderStyle.Fixed3D)]
    [DispId(PInvokeCore.DISPID_BORDERSTYLE)]
    [SRDescription(nameof(SR.UpDownBaseBorderStyleDescr))]
    public BorderStyle BorderStyle
    {
        get => _borderStyle;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            if (_borderStyle != value)
            {
                _borderStyle = value;
                RecreateHandle();
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the text property is being
    ///  changed internally by its parent class.
    /// </summary>
    protected bool ChangingText { get; set; }

    public override ContextMenuStrip? ContextMenuStrip
    {
        get => base.ContextMenuStrip;
        set
        {
            base.ContextMenuStrip = value;
            _upDownEdit.ContextMenuStrip = value;
        }
    }

    protected override AccessibleObject CreateAccessibilityInstance()
        => new UpDownBaseAccessibleObject(this);

    protected override CreateParams CreateParams
    {
        get
        {
            SetStyle(ControlStyles.ApplyThemingImplicitly, true);

            CreateParams cp = base.CreateParams;

            cp.Style &= ~(int)WINDOW_STYLE.WS_BORDER;

            if (!Application.RenderWithVisualStyles)
            {
                switch (_borderStyle)
                {
                    case BorderStyle.Fixed3D:
                        cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE;
                        break;
                    case BorderStyle.FixedSingle:
                        cp.Style |= (int)WINDOW_STYLE.WS_BORDER;
                        break;
                }
            }

            return cp;
        }
    }

    /// <summary>
    ///  When <see langword="true"/>, the up/down buttons are laid out side by side (decrement on the
    ///  leading edge, increment on the trailing edge) rather than stacked. This provides a larger,
    ///  more accessible click target and is used when a modern <see cref="VisualStylesMode"/>
    ///  (<see cref="VisualStylesMode.Net11"/> or above) is in effect.
    /// </summary>
    internal bool UseSideBySideButtons => EffectiveVisualStylesMode >= VisualStylesMode.Net11;

    /// <summary>
    ///  Deriving classes can override this to configure a default size for their control.
    ///  This is more efficient than setting the size in the control's constructor.
    /// </summary>
    protected override Size DefaultSize => new(DefaultControlWidth, PreferredHeight);

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new DockPaddingEdges DockPadding => base.DockPadding;

    /// <summary>
    ///  Returns true if this control has focus.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.ControlFocusedDescr))]
    public override bool Focused => _upDownEdit.Focused;

    /// <summary>
    ///  Indicates the foreground color for the control.
    /// </summary>
    public override Color ForeColor
    {
        get => _upDownEdit.ForeColor;
        set
        {
            base.ForeColor = value;
            _upDownEdit.ForeColor = value;
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the user can use the UP ARROW and
    ///  DOWN ARROW keys to select values.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.UpDownBaseInterceptArrowKeysDescr))]
    public bool InterceptArrowKeys { get; set; } = true;

    public override Size MaximumSize
    {
        get => base.MaximumSize;
        set => base.MaximumSize = new Size(value.Width, 0);
    }

    public override Size MinimumSize
    {
        get => base.MinimumSize;
        set => base.MinimumSize = new Size(value.Width, 0);
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? MouseEnter
    {
        add => base.MouseEnter += value;
        remove => base.MouseEnter -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? MouseLeave
    {
        add => base.MouseLeave += value;
        remove => base.MouseLeave -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? MouseHover
    {
        add => base.MouseHover += value;
        remove => base.MouseHover -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event MouseEventHandler? MouseMove
    {
        add => base.MouseMove += value;
        remove => base.MouseMove -= value;
    }

    /// <summary>
    ///  Gets the height of the up-down control.
    /// </summary>
    [SRCategory(nameof(SR.CatLayout))]
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.UpDownBasePreferredHeightDescr))]
    public int PreferredHeight
    {
        get
        {
            if (!UseSideBySideButtons)
            {
                int height = FontHeight;

                // Adjust for the border style
                if (_borderStyle != BorderStyle.None)
                {
                    height += SystemInformation.BorderSize.Height * 4 + 3;
                }
                else
                {
                    height += 3;
                }

                return height;
            }

            int contentInset = ModernContentInset;
            int preferredHeight = FontHeight + (contentInset * 2);

            if (_borderStyle == BorderStyle.Fixed3D)
            {
                int roundedChromeMinimumHeight = LogicalToDeviceUnits(ModernControlVisualStyles.UpDownCornerRadius)
                    + LogicalToDeviceUnits(ModernControlVisualStyles.BorderThickness)
                    + LogicalToDeviceUnits(ModernControlVisualStyles.InternalChromeInset);

                preferredHeight = Math.Max(preferredHeight, roundedChromeMinimumHeight);
            }

            return preferredHeight;
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the text may only be changed by
    ///  the use of the up or down buttons.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.UpDownBaseReadOnlyDescr))]
    public bool ReadOnly
    {
        get => _upDownEdit.ReadOnly;
        set => _upDownEdit.ReadOnly = value;
    }

    /// <summary>
    ///  Gets or sets the text displayed in the up-down control.
    /// </summary>
    [Localizable(true)]
    [AllowNull]
    public override string Text
    {
        get => _upDownEdit.Text;
        set
        {
            // The text changed event will at this point be triggered. After returning, the value of UserEdit will
            // reflect whether or not the current upDownEditbox text is in sync with any internally stored values.
            // If UserEdit is true, we must validate the text the user typed or set.

            _upDownEdit.Text = value;

            // Usually, the code in the Text changed event handler sets ChangingText back to false. If the text
            // hasn't actually changed though, the event handler never fires. ChangingText should always be false
            // on exit from this property.

            ChangingText = false;

            if (UserEdit)
            {
                ValidateEditText();
            }
        }
    }

    /// <summary>
    ///  Gets or sets the alignment of the text in the up-down control.
    /// </summary>
    [Localizable(true)]
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(HorizontalAlignment.Left)]
    [SRDescription(nameof(SR.UpDownBaseTextAlignDescr))]
    public HorizontalAlignment TextAlign
    {
        get => _upDownEdit.TextAlign;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            _upDownEdit.TextAlign = value;
        }
    }

    internal TextBox TextBox => _upDownEdit;

    /// <summary>
    ///  Gets or sets the alignment of the up and down buttons on the up-down control.
    /// </summary>
    [Localizable(true)]
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(LeftRightAlignment.Right)]
    [SRDescription(nameof(SR.UpDownBaseAlignmentDescr))]
    public LeftRightAlignment UpDownAlign
    {
        get => _upDownAlign;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            if (_upDownAlign != value)
            {
                _upDownAlign = value;
                PositionControls();
                Invalidate();
            }
        }
    }

    internal UpDownButtons UpDownButtonsInternal => _upDownButtons;

    /// <summary>
    ///  Gets or sets a value indicating whether a value has been entered by the user.
    /// </summary>
    protected bool UserEdit { get; set; }

    /// <summary>
    ///  When overridden in a derived class, handles the pressing of the down button
    ///  on the up-down control.
    /// </summary>
    public abstract void DownButton();

    internal override Rectangle ApplyBoundsConstraints(int suggestedX, int suggestedY, int proposedWidth, int proposedHeight)
    {
        int height = AutoSize
            ? PreferredHeight + Padding.Vertical
            : UseSideBySideButtons
                ? proposedHeight
                : PreferredHeight;

        return base.ApplyBoundsConstraints(suggestedX, suggestedY, proposedWidth, height);
    }

    internal override void ReleaseUiaProvider(HWND handle)
    {
        // UpDownEdit as TextBox is a control, that should disconnect its accessible object itself,
        // but if it supports Uia providers. If no, force disconnecting for UpDownEdit accessible object
        // as a part of UIA tree of Domain/NumericUpDown controls.
        if (!_upDownEdit.SupportsUiaProviders)
        {
            _upDownEdit.ReleaseUiaProvider(_upDownEdit.HWNDInternal);
        }

        base.ReleaseUiaProvider(handle);
    }

    /// <summary>
    ///  When overridden in a derived class, handles rescaling of any magic numbers used in control painting.
    ///  For UpDown controls, scale the width of the up/down buttons.
    ///  Must call the base class method to get the current DPI values. This method is invoked only when
    ///  Application opts-in into the Per-monitor V2 support, targets .NETFX 4.7 and has
    ///  EnableDpiChangedMessageHandling config switch turned on.
    /// </summary>
    protected override void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew)
    {
        base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew);
        _defaultButtonsWidth = LogicalToDeviceUnits(DefaultButtonsWidth);
        _upDownButtons.Width = _defaultButtonsWidth;
        CommonProperties.xClearPreferredSizeCache(this);

        if (AutoSize)
        {
            Height = PreferredHeight;
        }

        PositionControls();
    }

    /// <summary>
    ///  When overridden in a derived class, raises the Changed event.
    /// </summary>
    protected virtual void OnChanged(object? source, EventArgs e)
    {
    }

    /// <summary>
    ///  Initialize the updown. Adds the upDownEdit and updown buttons.
    /// </summary>
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        PositionControls();
        SystemEvents.UserPreferenceChanged += UserPreferenceChanged;
    }

    /// <summary>
    ///  Tear down the updown.
    /// </summary>
    protected override void OnHandleDestroyed(EventArgs e)
    {
        SystemEvents.UserPreferenceChanged -= UserPreferenceChanged;
        _focusIndicatorRenderer?.Dispose();
        _focusIndicatorRenderer = null;
        base.OnHandleDestroyed(e);
    }

    /// <inheritdoc/>
    protected override void OnVisualStylesModeChanged(EventArgs e)
    {
        base.OnVisualStylesModeChanged(e);
        _focusIndicatorRenderer?.Synchronize(Focused, invalidate: false);
        CommonProperties.xClearPreferredSizeCache(this);

        if (AutoSize)
        {
            Height = PreferredHeight;
        }

        PositionControls();

        if (IsHandleCreated)
        {
            Invalidate(true);
        }
    }

    /// <summary>
    ///  Handles painting the buttons on the control.
    /// </summary>
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        if (UseSideBySideButtons)
        {
            // In modern mode we draw a single frame around the whole control (edit and buttons); the
            // themed/classic single-textbox border below does not apply.
            DrawModernBorder(e);
            return;
        }

        Rectangle editBounds = _upDownEdit.Bounds;
        Color backColor = BackColor;

        if (Application.RenderWithVisualStyles)
        {
            if (_borderStyle != BorderStyle.None)
            {
                Rectangle bounds = ClientRectangle;
                Rectangle clipBounds = e.ClipRectangle;

                // Draw a themed textbox-like border, which is what the spin control does
                VisualStyleRenderer vsr = new(VisualStyleElement.TextBox.TextEdit.Normal);
                int border = ThemedBorderWidth;
                Rectangle clipLeft = new(bounds.Left, bounds.Top, border, bounds.Height);
                Rectangle clipTop = new(bounds.Left, bounds.Top, bounds.Width, border);
                Rectangle clipRight = new(bounds.Right - border, bounds.Top, border, bounds.Height);
                Rectangle clipBottom = new(bounds.Left, bounds.Bottom - border, bounds.Width, border);
                clipLeft.Intersect(clipBounds);
                clipTop.Intersect(clipBounds);
                clipRight.Intersect(clipBounds);
                clipBottom.Intersect(clipBounds);

                using DeviceContextHdcScope hdc = new(e);
                vsr.DrawBackground(hdc, bounds, clipLeft, HWNDInternal);
                vsr.DrawBackground(hdc, bounds, clipTop, HWNDInternal);
                vsr.DrawBackground(hdc, bounds, clipRight, HWNDInternal);
                vsr.DrawBackground(hdc, bounds, clipBottom, HWNDInternal);

                // Draw a rectangle around edit control with the background color.
                Rectangle backRect = editBounds;
                backRect.X--;
                backRect.Y--;
                backRect.Width += 2;
                backRect.Height += 2;
                using CreatePenScope hpen = new(backColor);
                hdc.DrawRectangle(backRect, hpen);
            }
        }
        else
        {
            // Draw a rectangle around edit control with the background color.
            Rectangle backRect = editBounds;
            backRect.Inflate(1, 1);
            if (!Enabled)
            {
                backRect.X--;
                backRect.Y--;
                backRect.Width++;
                backRect.Height++;
            }

            int width = Enabled ? 2 : 1;

            backRect.Width++;
            backRect.Height++;
            using DeviceContextHdcScope hdc = new(e);
            using CreatePenScope hpen = new(backColor, width);
            hdc.DrawRectangle(backRect, hpen);
        }

        if (!Enabled && BorderStyle != BorderStyle.None && !_upDownEdit.ShouldSerializeBackColor())
        {
            // Draws a grayed rectangle around the upDownEdit, since otherwise we will have a white
            // border around the upDownEdit, which is inconsistent with Windows' behavior
            // we only want to do this when BackColor is not serialized, since otherwise
            // we should display the BackColor instead of the usual grayed textbox.
            editBounds.Inflate(1, 1);
            ControlPaint.DrawBorderSimple(e, editBounds, SystemColors.Control);
        }
    }

    /// <summary>
    ///  Raises the <see cref="Control.KeyDown"/> event.
    /// </summary>
    protected virtual void OnTextBoxKeyDown(object? source, KeyEventArgs e)
    {
        OnKeyDown(e);
        if (InterceptArrowKeys)
        {
            // Intercept up arrow
            if (e.KeyData == Keys.Up)
            {
                UpButton();
                e.Handled = true;
            }

            // Intercept down arrow
            else if (e.KeyData == Keys.Down)
            {
                DownButton();
                e.Handled = true;
            }
        }

        // Perform text validation if ENTER is pressed
        if (e.KeyCode == Keys.Return && UserEdit)
        {
            ValidateEditText();
        }
    }

    /// <summary>
    ///  Raises the <see cref="Control.KeyPress"/> event.
    /// </summary>
    protected virtual void OnTextBoxKeyPress(object? source, KeyPressEventArgs e)
        => OnKeyPress(e);

    /// <summary>
    ///  Raises the <see cref="Control.LostFocus"/> event.
    /// </summary>
    protected virtual void OnTextBoxLostFocus(object? source, EventArgs e)
    {
        if (UserEdit)
        {
            ValidateEditText();
        }
    }

    /// <summary>
    ///  Raises the <see cref="Control.Resize"/> event.
    /// </summary>
    protected virtual void OnTextBoxResize(object? source, EventArgs e)
    {
        if (!UseSideBySideButtons || AutoSize)
        {
            Height = PreferredHeight;
        }

        PositionControls();
    }

    /// <summary>
    ///  Raises the TextBoxTextChanged event.
    ///  event.
    /// </summary>
    protected virtual void OnTextBoxTextChanged(object? source, EventArgs e)
    {
        if (ChangingText)
        {
            Debug.Assert(!UserEdit, "OnTextBoxTextChanged() - UserEdit == true");
            ChangingText = false;
        }
        else
        {
            UserEdit = true;
        }

        OnTextChanged(e);
        OnChanged(source, EventArgs.Empty);
    }

    /// <summary>
    ///  Called from the UpDownButtons member. Provided for derived controls to have a finer way to handle the event.
    /// </summary>
    internal virtual void OnStartTimer()
    {
    }

    internal virtual void OnStopTimer()
    {
    }

    /// <summary>
    ///  Raises the <see cref="Control.OnMouseDown"/> event.
    /// </summary>
    protected override void OnMouseDown(MouseEventArgs e)
    {
        if (e.Clicks == 2 && e.Button == MouseButtons.Left)
        {
            _doubleClickFired = true;
        }

        base.OnMouseDown(e);
    }

    /// <summary>
    ///  Raises the <see cref="Control.OnMouseUp"/> event.
    /// </summary>
    protected override void OnMouseUp(MouseEventArgs mevent)
    {
        if (mevent.Button == MouseButtons.Left)
        {
            if (PInvoke.WindowFromPoint(PointToScreen(mevent.Location)) == HWND && !ValidationCancelled)
            {
                if (!_doubleClickFired)
                {
                    OnClick(mevent);
                    OnMouseClick(mevent);
                }
                else
                {
                    _doubleClickFired = false;
                    OnDoubleClick(mevent);
                    OnMouseDoubleClick(mevent);
                }
            }

            _doubleClickFired = false;
        }

        base.OnMouseUp(mevent);
    }

    /// <summary>
    ///  Raises the <see cref="Control.OnMouseWheel"/> event.
    /// </summary>
    protected override void OnMouseWheel(MouseEventArgs e)
    {
        base.OnMouseWheel(e);
        if (e is HandledMouseEventArgs hme)
        {
            if (hme.Handled)
            {
                return;
            }

            hme.Handled = true;
        }

        if ((ModifierKeys & (Keys.Shift | Keys.Alt)) != 0 || MouseButtons != MouseButtons.None)
        {
            // Do not scroll when Shift or Alt key is down, or when a mouse button is down.
            return;
        }

        int wheelScrollLines = SystemInformation.MouseWheelScrollLines;
        if (wheelScrollLines == 0)
        {
            // Do not scroll when the user system setting is 0 lines per notch
            return;
        }

        Debug.Assert(_wheelDelta > -PInvoke.WHEEL_DELTA, "wheelDelta is too small");
        Debug.Assert(_wheelDelta < PInvoke.WHEEL_DELTA, "wheelDelta is too big");
        _wheelDelta += e.Delta;

        float partialNotches;
        partialNotches = _wheelDelta / (float)PInvoke.WHEEL_DELTA;

        if (wheelScrollLines == -1)
        {
            wheelScrollLines = DefaultWheelScrollLinesPerPage;
        }

        // Evaluate number of bands to scroll
        int scrollBands = (int)(wheelScrollLines * partialNotches);

        if (scrollBands != 0)
        {
            int absScrollBands;
            if (scrollBands > 0)
            {
                absScrollBands = scrollBands;
                while (absScrollBands > 0)
                {
                    UpButton();
                    absScrollBands--;
                }

                _wheelDelta -= (int)(scrollBands * (PInvoke.WHEEL_DELTA / (float)wheelScrollLines));
            }
            else
            {
                absScrollBands = -scrollBands;
                while (absScrollBands > 0)
                {
                    DownButton();
                    absScrollBands--;
                }

                _wheelDelta -= (int)(scrollBands * (PInvoke.WHEEL_DELTA / (float)wheelScrollLines));
            }
        }
    }

    /// <summary>
    ///  Handle the layout event. The size of the upDownEdit control, and the
    ///  position of the UpDown control must be modified.
    /// </summary>
    protected override void OnLayout(LayoutEventArgs e)
    {
        PositionControls();
        base.OnLayout(e);
    }

    /// <summary>
    ///  Raises the FontChanged event.
    /// </summary>
    protected override void OnFontChanged(EventArgs e)
    {
        // Clear the font height cache
        FontHeight = -1;

        if (!UseSideBySideButtons || AutoSize)
        {
            Height = PreferredHeight;
        }

        PositionControls();

        base.OnFontChanged(e);
    }

    /// <summary>
    ///  Handles UpDown events, which are generated by clicking on the updown
    ///  buttons in the child updown control.
    /// </summary>
    private void OnUpDown(object? source, UpDownEventArgs e)
    {
        // Modify the value
        if (e.ButtonID == (int)ButtonID.Up)
        {
            UpButton();
        }
        else if (e.ButtonID == (int)ButtonID.Down)
        {
            DownButton();
        }
    }

    /// <summary>
    ///  Calculates the size and position of the upDownEdit control and the updown buttons.
    /// </summary>
    private void PositionControls()
    {
        if (UseSideBySideButtons)
        {
            PositionControlsModern();
            return;
        }

        Rectangle upDownEditBounds = Rectangle.Empty;
        Rectangle upDownButtonsBounds = Rectangle.Empty;

        Rectangle clientArea = LayoutUtils.DeflateRect(
            new Rectangle(Point.Empty, ClientSize),
            Padding);

        bool themed = Application.RenderWithVisualStyles;
        BorderStyle borderStyle = BorderStyle;

        // Determine how much to squish in - Fixed3D and FixedSingle have 2PX border
        int borderWidth = (borderStyle == BorderStyle.None) ? 0 : 2;
        clientArea.Inflate(-borderWidth, -borderWidth);

        // Reposition and resize the upDownEdit control
        if (_upDownEdit is not null)
        {
            upDownEditBounds = clientArea;
            upDownEditBounds.Size = new Size(clientArea.Width - _defaultButtonsWidth, clientArea.Height);
        }

        // Reposition and resize the updown buttons
        if (_upDownButtons is not null)
        {
            int borderFixup = (themed) ? 1 : 2;

            if (borderStyle == BorderStyle.None)
            {
                borderFixup = 0;
            }

            upDownButtonsBounds = new Rectangle(
                clientArea.Right - _defaultButtonsWidth + borderFixup,
                clientArea.Top - borderFixup,
                _defaultButtonsWidth,
                clientArea.Height + (borderFixup * 2));
        }

        // Right to left translation
        LeftRightAlignment updownAlign = UpDownAlign;
        updownAlign = RtlTranslateLeftRight(updownAlign);

        // Left/right updown align translation
        if (updownAlign == LeftRightAlignment.Left)
        {
            // If the buttons are aligned to the left, swap position of text box/buttons
            upDownButtonsBounds.X = clientArea.Left + (clientArea.Right - upDownButtonsBounds.Right);
            upDownEditBounds.X = clientArea.Left + (clientArea.Right - upDownEditBounds.Right);
        }

        // Apply locations
        _upDownEdit?.Bounds = upDownEditBounds;

        if (_upDownButtons is not null)
        {
            _upDownButtons.Bounds = upDownButtonsBounds;
            _upDownButtons.Invalidate();
        }
    }

    private int ModernContentInset
        // Match the per-border-style internal padding a modern TextBoxBase applies (see
        // ModernControlVisualStyles.GetFieldPadding): the border-padding component depends on the
        // border style rather than the flat 1px border stroke. This keeps the edit/button content
        // inset one pixel inside the frame for a Fixed3D border, aligning UpDownBase with the other
        // modern controls (Fixed3D = 2 + 2, FixedSingle = 1 + 2, None = 1 + 2).
        => LogicalToDeviceUnits(
            (_borderStyle switch
            {
                BorderStyle.Fixed3D => ModernControlVisualStyles.Fixed3DBorderPadding,
                BorderStyle.FixedSingle => ModernControlVisualStyles.FixedSingleBorderPadding,
                _ => ModernControlVisualStyles.NoBorderPadding,
            })
            + ModernControlVisualStyles.InternalChromeInset);

    internal int ModernButtonGroupSpacing
        => LogicalToDeviceUnits(ModernButtonGroupSpacingLogical);

    internal int GetModernButtonGroupWidth()
        => (_defaultButtonsWidth * 2) + ModernButtonGroupSpacing;

    internal int GetPreferredWidth(int textWidth, int height)
        => UseSideBySideButtons
            ? textWidth + (ModernContentInset * 2) + GetModernButtonGroupWidth()
            : SizeFromClientSizeInternal(new(textWidth, height)).Width + _upDownButtons.Width;

    /// <summary>
    ///  Calculates the size and position of the upDownEdit control and the side-by-side updown buttons
    ///  when a modern <see cref="VisualStylesMode"/> is in effect. Both the edit and the buttons are
    ///  inset by <see cref="ModernContentInset"/> so they sit inside the frame drawn by
    ///  <see cref="OnPaint"/> and clear its rounded corners.
    /// </summary>
    private void PositionControlsModern()
    {
        Rectangle clientArea = LayoutUtils.DeflateRect(
            new Rectangle(Point.Empty, ClientSize),
            Padding);

        int pad = ModernContentInset;
        int buttonsWidth = Math.Min(GetModernButtonGroupWidth(), Math.Max(0, clientArea.Width - (pad * 2)));

        Rectangle inner = clientArea;
        inner.Inflate(-pad, -pad);

        if (inner.Width < 0 || inner.Height < 0)
        {
            inner = new Rectangle(
                x: Math.Min(pad, clientArea.Width),
                y: Math.Min(pad, clientArea.Height),
                width: 0,
                height: 0);
        }

        Rectangle upDownEditBounds = inner;
        upDownEditBounds.Width = Math.Max(0, inner.Width - buttonsWidth);

        Rectangle upDownButtonsBounds = new(
            x: inner.Right - buttonsWidth,
            y: inner.Top,
            width: buttonsWidth,
            height: inner.Height);

        // Left/right updown align translation (also honors RTL).
        if (RtlTranslateLeftRight(UpDownAlign) == LeftRightAlignment.Left)
        {
            upDownButtonsBounds.X = clientArea.Left + (clientArea.Right - upDownButtonsBounds.Right);
            upDownEditBounds.X = clientArea.Left + (clientArea.Right - upDownEditBounds.Right);
        }

        _upDownEdit?.Bounds = upDownEditBounds;

        if (_upDownButtons is not null)
        {
            _upDownButtons.Bounds = upDownButtonsBounds;
            _upDownButtons.Invalidate();
        }
    }

    /// <summary>
    ///  Draws the modern frame around the whole control (edit and side-by-side buttons) using the same
    ///  rounded/flat chrome a stand-alone modern TextBox paints in its non-client area. The corners are
    ///  filled with the parent's back color so the rounded frame blends against it.
    /// </summary>
    private void DrawModernBorder(PaintEventArgs e)
    {
        Rectangle bounds = ClientRectangle;

        if (bounds.Width <= 0 || bounds.Height <= 0)
        {
            return;
        }

        int cornerRadius = LogicalToDeviceUnits(ModernControlVisualStyles.UpDownCornerRadius);
        int borderThickness = LogicalToDeviceUnits(ModernControlVisualStyles.BorderThickness);

        // The adorner (border) color matches the modern TextBox chrome, which uses the fore color.
        Color adornerColor = ForeColor;
        Color parentBackColor = Parent?.BackColor ?? BackColor;
        Color clientBackColor = BackColor;

        using var clientBackgroundBrush = clientBackColor.GetCachedSolidBrushScope();
        using var adornerPen = adornerColor.GetCachedPenScope(borderThickness);

        Rectangle deflatedBounds = bounds;
        deflatedBounds.Width -= 1;
        deflatedBounds.Height -= 1;

        Graphics graphics = e.Graphics;
        using GraphicsStateScope graphicsState = new(graphics);
        graphics.SmoothingMode = SmoothingMode.AntiAlias;

        // AddRoundedRectangle receives the bounding size of each corner arc, so one corner size plus
        // the border thickness is the minimum height that avoids overlapping curves.
        bool canRenderRoundedChrome = TextBoxBase.CanRenderVisualStylesRoundedChrome(
            deflatedBounds,
            cornerRadius,
            borderThickness);

        ParentBackgroundRenderer.Paint(this, graphics, bounds, parentBackColor);

        switch (_borderStyle)
        {
            case BorderStyle.None:
                graphics.FillRectangle(clientBackgroundBrush, deflatedBounds);
                break;

            case BorderStyle.FixedSingle:
                graphics.FillRectangle(clientBackgroundBrush, deflatedBounds);
                graphics.DrawRectangle(adornerPen, deflatedBounds);
                break;

            case BorderStyle.Fixed3D:
                if (canRenderRoundedChrome)
                {
                    using GraphicsPath bodyPath = new();
                    bodyPath.AddRoundedRectangle(deflatedBounds, new Size(cornerRadius, cornerRadius));
                    graphics.FillPath(clientBackgroundBrush, bodyPath);
                    graphics.DrawPath(adornerPen, bodyPath);

                    // The rounded chrome is clipped with a non-antialiased region; blend the resulting
                    // corner artifacts into the parent by tracing the parent color just outside the border.
                    ParentBackgroundRenderer.PaintRoundedBorderRegionMitigation(
                        graphics,
                        deflatedBounds,
                        new Size(cornerRadius, cornerRadius),
                        borderThickness,
                        parentBackColor);
                }
                else
                {
                    graphics.FillRectangle(clientBackgroundBrush, deflatedBounds);
                    graphics.DrawRectangle(adornerPen, deflatedBounds);
                }

                break;
        }

        if (_borderStyle == BorderStyle.Fixed3D && canRenderRoundedChrome)
        {
            Color focusColor = ModernFocusColor;
            FocusIndicatorRenderer.DrawRoundedFocusIndicator(
                graphics,
                deflatedBounds,
                cornerRadius,
                borderThickness,
                LogicalToDeviceUnits(ModernFocusBandHeight),
                adornerColor,
                focusColor);
        }
        else if (Focused && _borderStyle == BorderStyle.Fixed3D)
        {
            Color focusColor = ModernFocusColor;
            using var focusPen = focusColor.GetCachedPenScope(borderThickness);
            graphics.DrawLine(
                focusPen,
                deflatedBounds.Left,
                deflatedBounds.Bottom,
                deflatedBounds.Right,
                deflatedBounds.Bottom);
            graphics.DrawLine(
                focusPen,
                deflatedBounds.Left,
                deflatedBounds.Bottom - 1,
                deflatedBounds.Right,
                deflatedBounds.Bottom - 1);
        }
    }

    private AnimatedFocusIndicatorRenderer FocusIndicatorRenderer
        => _focusIndicatorRenderer ??= new(this, InvalidateModernFocusIndicator);

    private static Color ModernFocusColor
        => TextBoxBase.GetVisualStylesFocusColor(Application.SystemVisualSettings.HighContrastEnabled);

    private void SetModernFocusState(bool focused)
    {
        if (!UseSideBySideButtons || _borderStyle != BorderStyle.Fixed3D)
        {
            return;
        }

        FocusIndicatorRenderer.SetFocused(
            focused,
            animate: SystemInformation.UIEffectsEnabled && !SystemInformation.HighContrast);
    }

    private void InvalidateModernFocusIndicator()
    {
        int focusBandHeight = LogicalToDeviceUnits(ModernFocusBandHeight);
        Rectangle focusBounds = ClientRectangle;
        focusBounds.Y = Math.Max(focusBounds.Top, focusBounds.Bottom - focusBandHeight);
        focusBounds.Height = Math.Min(focusBandHeight, focusBounds.Height);
        Invalidate(focusBounds);
    }

    /// <summary>
    ///  Selects a range of text in the up-down control.
    /// </summary>
    public void Select(int start, int length)
        => _upDownEdit.Select(start, length);

    /// <summary>
    ///  Create a new <see cref="MouseEventArgs"/> with the points translated from the <paramref name="child"/>
    ///  coordinates to this control's.
    /// </summary>
    private MouseEventArgs TranslateMouseEvent(Control child, MouseEventArgs e)
    {
        if (child is not null && IsHandleCreated)
        {
            // Same control as PointToClient or PointToScreen, just
            // with two specific controls in mind.
            Point point = e.Location;
            point = WindowsFormsUtils.TranslatePoint(point, child, this);
            return new MouseEventArgs(e.Button, e.Clicks, point.X, point.Y, e.Delta);
        }

        return e;
    }

    /// <summary>
    ///  When overridden in a derived class, handles the pressing of the up button on the up-down control.
    /// </summary>
    public abstract void UpButton();

    /// <summary>
    ///  When overridden in a derived class, updates the text displayed in the up-down control.
    /// </summary>
    protected abstract void UpdateEditText();

    private void UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs pref)
    {
        if (pref.Category == UserPreferenceCategory.Locale)
        {
            UpdateEditText();
        }
    }

    /// <summary>
    ///  When overridden in a derived class, validates the text displayed in the up-down control.
    /// </summary>
    protected virtual void ValidateEditText()
    {
    }

    [EditorBrowsable(EditorBrowsableState.Advanced)]
    protected override void WndProc(ref Message m)
    {
        switch (m.MsgInternal)
        {
            case PInvokeCore.WM_SETFOCUS:
                if (!HostedInWin32DialogManager)
                {
                    if (ActiveControl is null)
                    {
                        SetActiveControl(TextBox);
                    }
                    else
                    {
                        FocusActiveControlInternal();
                    }
                }
                else
                {
                    if (TextBox.CanFocus)
                    {
                        PInvoke.SetFocus(TextBox);
                    }

                    base.WndProc(ref m);
                }

                break;
            case PInvokeCore.WM_KILLFOCUS:
                DefWndProc(ref m);
                break;
            default:
                base.WndProc(ref m);
                break;
        }
    }

    internal override void SetToolTip(ToolTip toolTip)
    {
        if (toolTip is null)
        {
            return;
        }

        string? caption = toolTip.GetToolTip(this);
        toolTip.SetToolTip(_upDownEdit, caption);
        toolTip.SetToolTip(_upDownButtons, caption);
    }

    internal override void RemoveToolTip(ToolTip toolTip)
    {
        if (toolTip is null)
        {
            return;
        }

        string? caption = toolTip.GetToolTip(this);
        toolTip.SetToolTip(_upDownEdit, caption);
        toolTip.SetToolTip(_upDownButtons, caption);
    }
}
