// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms.VisualStyles;
using static Interop;

namespace System.Windows.Forms
{
    public abstract partial class UpDownBase
    {
        /// <summary>
        ///  A control representing the pair of buttons on the end of the upDownEdit control. This class handles
        ///  drawing the updown buttons, and detecting mouse actions on these buttons. Acceleration on the buttons is
        ///  handled. The control sends UpDownEventArgss to the parent UpDownBase class when a button is pressed, or
        ///  when the acceleration determines that another event should be generated.
        /// </summary>
        internal partial class UpDownButtons : Control
        {
            private readonly UpDownBase _parent;

            // Button state
            private ButtonID _pushed = ButtonID.None;
            private ButtonID _captured = ButtonID.None;
            private ButtonID _mouseOver = ButtonID.None;

            private UpDownEventHandler _upDownEventHandler;

            private Timer _timer; // generates UpDown events
            private int _timerInterval; // milliseconds between events

            private bool _doubleClickFired;

            internal UpDownButtons(UpDownBase parent) : base()
            {
                SetStyle(ControlStyles.Opaque | ControlStyles.FixedHeight | ControlStyles.FixedWidth, true);
                SetStyle(ControlStyles.Selectable, false);

                _parent = parent;
            }

            /// <summary>
            ///  Adds a handler for the updown button event.
            /// </summary>
            public event UpDownEventHandler UpDown
            {
                add => _upDownEventHandler += value;
                remove => _upDownEventHandler -= value;
            }

            /// <remarks>
            ///  Called when the mouse button is pressed - we need to start spinning the value of the updown.
            /// </remarks>
            private void BeginButtonPress(MouseEventArgs e)
            {
                int half_height = Size.Height / 2;

                if (e.Y < half_height)
                {
                    // Up button
                    _pushed = _captured = ButtonID.Up;
                    Invalidate();
                }
                else
                {
                    // Down button
                    _pushed = _captured = ButtonID.Down;
                    Invalidate();
                }

                // Capture the mouse
                Capture = true;

                // Generate UpDown event
                OnUpDown(new UpDownEventArgs((int)_pushed));

                // Start the timer for new updown events
                StartTimer();
            }

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

            /// <remarks>
            ///  Called when the mouse button is released - we need to stop spinning the value of the updown.
            /// </remarks>
            private void EndButtonPress()
            {
                _pushed = ButtonID.None;
                _captured = ButtonID.None;

                // Stop the timer
                StopTimer();

                // Release the mouse
                Capture = false;

                // Redraw the buttons
                Invalidate();
            }

            /// <summary>
            ///  Handles detecting mouse hits on the buttons. This method detects
            ///  which button was hit (up or down), fires a updown event, captures
            ///  the mouse, and starts a timer for repeated updown events.
            /// </summary>
            protected override void OnMouseDown(MouseEventArgs e)
            {
                // Begin spinning the value
                // Focus the parent
                _parent.Focus();

                if (!_parent.ValidationCancelled && e.Button == MouseButtons.Left)
                {
                    BeginButtonPress(e);
                }

                if (e.Clicks == 2 && e.Button == MouseButtons.Left)
                {
                    _doubleClickFired = true;
                }

                // At no stage should a button be pushed, and the mouse
                // not captured.
                Debug.Assert(
                    !(_pushed != ButtonID.None && _captured == ButtonID.None),
                    "Invalid button pushed/captured combination");

                _parent.OnMouseDown(_parent.TranslateMouseEvent(this, e));
            }

            protected override void OnMouseMove(MouseEventArgs e)
            {
                // If the mouse is captured by the buttons (i.e. an updown button
                // was pushed, and the mouse button has not yet been released),
                // determine the new state of the buttons depending on where
                // the mouse pointer has moved.

                if (Capture)
                {
                    // Determine button area
                    Rectangle rect = ClientRectangle;
                    rect.Height /= 2;

                    if (_captured == ButtonID.Down)
                    {
                        rect.Y += rect.Height;
                    }

                    // Test if the mouse has moved outside the button area
                    if (rect.Contains(e.X, e.Y))
                    {
                        // Inside button, repush the button if necessary

                        if (_pushed != _captured)
                        {
                            // Restart the timer
                            StartTimer();

                            _pushed = _captured;
                            Invalidate();
                        }
                    }
                    else
                    {
                        // Outside button
                        //
                        // Retain the capture, but pop the button up whilst the mouse
                        // remains outside the button and the mouse button remains pressed.
                        if (_pushed != ButtonID.None)
                        {
                            // Stop the timer for updown events
                            StopTimer();

                            _pushed = ButtonID.None;
                            Invalidate();
                        }
                    }
                }

                // Logic for seeing which button is Hot if any
                Rectangle rectUp = ClientRectangle, rectDown = ClientRectangle;
                rectUp.Height /= 2;
                rectDown.Y += rectDown.Height / 2;

                // Check if the mouse is on the upper or lower button. Note that it could be in neither.
                if (rectUp.Contains(e.X, e.Y))
                {
                    _mouseOver = ButtonID.Up;
                    Invalidate();
                }
                else if (rectDown.Contains(e.X, e.Y))
                {
                    _mouseOver = ButtonID.Down;
                    Invalidate();
                }

                // At no stage should a button be pushed, and the mouse not captured.
                Debug.Assert(
                    !(_pushed != ButtonID.None && _captured == ButtonID.None),
                    "Invalid button pushed/captured combination");

                _parent.OnMouseMove(_parent.TranslateMouseEvent(this, e));
            }

            /// <summary>
            ///  Handles detecting when the mouse button is released.
            /// </summary>
            protected override void OnMouseUp(MouseEventArgs e)
            {
                if (!_parent.ValidationCancelled && e.Button == MouseButtons.Left)
                {
                    EndButtonPress();
                }

                // At no stage should a button be pushed, and the mouse not captured.
                Debug.Assert(
                    !(_pushed != ButtonID.None && _captured == ButtonID.None),
                    "Invalid button pushed/captured combination");

                Point pt = new Point(e.X, e.Y);
                pt = PointToScreen(pt);

                MouseEventArgs me = _parent.TranslateMouseEvent(this, e);
                if (e.Button == MouseButtons.Left)
                {
                    if (!_parent.ValidationCancelled && User32.WindowFromPoint(pt) == Handle)
                    {
                        if (!_doubleClickFired)
                        {
                            _parent.OnClick(me);
                        }
                        else
                        {
                            _doubleClickFired = false;
                            _parent.OnDoubleClick(me);
                            _parent.OnMouseDoubleClick(me);
                        }
                    }

                    _doubleClickFired = false;
                }

                _parent.OnMouseUp(me);
            }

            protected override void OnMouseLeave(EventArgs e)
            {
                _mouseOver = ButtonID.None;
                Invalidate();

                _parent.OnMouseLeave(e);
            }

            /// <summary>
            ///  Handles painting the buttons on the control.
            /// </summary>
            protected override void OnPaint(PaintEventArgs e)
            {
                int half_height = ClientSize.Height / 2;

                // Draw the up and down buttons
                if (Application.RenderWithVisualStyles)
                {
                    var vsr = new VisualStyleRenderer(_mouseOver == ButtonID.Up
                        ? VisualStyleElement.Spin.Up.Hot
                        : VisualStyleElement.Spin.Up.Normal);

                    if (!Enabled)
                    {
                        vsr.SetParameters(VisualStyleElement.Spin.Up.Disabled);
                    }
                    else if (_pushed == ButtonID.Up)
                    {
                        vsr.SetParameters(VisualStyleElement.Spin.Up.Pressed);
                    }

                    using var hdc = new DeviceContextHdcScope(e);
                    vsr.DrawBackground(
                        hdc,
                        new Rectangle(0, 0, _parent._defaultButtonsWidth, half_height),
                        HandleInternal);

                    if (!Enabled)
                    {
                        vsr.SetParameters(VisualStyleElement.Spin.Down.Disabled);
                    }
                    else if (_pushed == ButtonID.Down)
                    {
                        vsr.SetParameters(VisualStyleElement.Spin.Down.Pressed);
                    }
                    else
                    {
                        vsr.SetParameters(_mouseOver == ButtonID.Down
                            ? VisualStyleElement.Spin.Down.Hot
                            : VisualStyleElement.Spin.Down.Normal);
                    }

                    vsr.DrawBackground(
                        hdc,
                        new Rectangle(0, half_height, _parent._defaultButtonsWidth, half_height),
                        HandleInternal);
                }
                else
                {
                    ControlPaint.DrawScrollButton(
                        e.GraphicsInternal,
                        new Rectangle(0, 0, _parent._defaultButtonsWidth, half_height),
                        ScrollButton.Up,
                        _pushed == ButtonID.Up ? ButtonState.Pushed : (Enabled ? ButtonState.Normal : ButtonState.Inactive));

                    ControlPaint.DrawScrollButton(
                        e.GraphicsInternal,
                        new Rectangle(0, half_height, _parent._defaultButtonsWidth, half_height),
                        ScrollButton.Down,
                        _pushed == ButtonID.Down ? ButtonState.Pushed : (Enabled ? ButtonState.Normal : ButtonState.Inactive));
                }

                if (half_height != (ClientSize.Height + 1) / 2)
                {
                    // When control has odd height, a line needs to be drawn below the buttons with the backcolor.

                    Color color = _parent.BackColor;

                    Rectangle clientRect = ClientRectangle;
                    Point pt1 = new Point(clientRect.Left, clientRect.Bottom - 1);
                    Point pt2 = new Point(clientRect.Right, clientRect.Bottom - 1);

                    using var hdc = new DeviceContextHdcScope(e);
                    using var hpen = new Gdi32.CreatePenScope(color);
                    hdc.DrawLine(hpen, pt1, pt2);
                }

                // Raise the paint event, just in case this inner class goes public some day
                base.OnPaint(e);
            }

            /// <summary>
            ///  Occurs when the UpDown buttons are pressed and when the acceleration timer tick event is raised.
            /// </summary>
            protected virtual void OnUpDown(UpDownEventArgs upevent)
                => _upDownEventHandler?.Invoke(this, upevent);

            /// <summary>
            ///  Starts the timer for generating updown events
            /// </summary>
            protected void StartTimer()
            {
                _parent.OnStartTimer();
                if (_timer is null)
                {
                    // Generates UpDown events
                    _timer = new Timer();
                    // Add the timer handler
                    _timer.Tick += new EventHandler(TimerHandler);
                }

                _timerInterval = DefaultTimerInterval;

                _timer.Interval = _timerInterval;
                _timer.Start();
            }

            /// <summary>
            ///  Stops the timer for generating updown events
            /// </summary>
            protected void StopTimer()
            {
                if (_timer is not null)
                {
                    _timer.Stop();
                    _timer.Dispose();
                    _timer = null;
                }

                _parent.OnStopTimer();
            }

            internal override bool SupportsUiaProviders => true;

            /// <summary>
            ///  Generates updown events when the timer calls this function.
            /// </summary>
            private void TimerHandler(object source, EventArgs args)
            {
                // Make sure we've got mouse capture
                if (!Capture)
                {
                    EndButtonPress();
                    return;
                }

                // onUpDown method calls customer's ValueCHanged event handler which might enter the message loop and
                // process the mouse button up event, which results in timer being disposed
                OnUpDown(new UpDownEventArgs((int)_pushed));

                if (_timer is not null)
                {
                    // Accelerate timer.
                    _timerInterval *= 7;
                    _timerInterval /= 10;

                    if (_timerInterval < 1)
                    {
                        _timerInterval = 1;
                    }

                    _timer.Interval = _timerInterval;
                }
            }
        }
    }
}
