// 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 Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;

namespace System.Windows.Forms;

/// <summary>
///  Implements the basic functionality of a scroll bar control.
/// </summary>
[DefaultProperty(nameof(Value))]
[DefaultEvent(nameof(Scroll))]
public abstract partial class ScrollBar : Control
{
    private static readonly object s_scrollEvent = new();
    private static readonly object s_valueChangedEvent = new();

    private int _minimum;
    private int _maximum = 100;
    private int _smallChange = 1;
    private int _largeChange = 10;
    private int _value;
    private readonly ScrollOrientation _scrollOrientation;
    private int _wheelDelta;

    /// <summary>
    ///  Initializes a new instance of the <see cref="ScrollBar"/> class.
    /// </summary>
    public ScrollBar() : base()
    {
        SetStyle(ControlStyles.UserPaint, false);
        SetStyle(ControlStyles.StandardClick, false);
        SetStyle(ControlStyles.UseTextForAccessibility, false);

#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
        SetStyle(ControlStyles.ApplyThemingImplicitly, true);
#pragma warning restore WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

        TabStop = false;

        if ((CreateParams.Style & (int)SCROLLBAR_CONSTANTS.SB_VERT) != 0)
        {
            _scrollOrientation = ScrollOrientation.VerticalScroll;
        }
        else
        {
            _scrollOrientation = ScrollOrientation.HorizontalScroll;
        }
    }

    /// <summary>
    ///  Hide AutoSize: it doesn't make sense for this control
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public override bool AutoSize
    {
        get => base.AutoSize;
        set => base.AutoSize = value;
    }

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

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override Color BackColor
    {
        get => base.BackColor;
        set => base.BackColor = value;
    }

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

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    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)]
    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;
    }

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ClassName = PInvoke.WC_SCROLLBAR;
            cp.Style &= ~(int)WINDOW_STYLE.WS_BORDER;
            return cp;
        }
    }

    protected override ImeMode DefaultImeMode => ImeMode.Disable;

    protected override Padding DefaultMargin => Padding.Empty;

    protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
    {
        // Skip scaling if opted out.
        if (!ScaleScrollBarForDpiChange)
        {
            return;
        }

        base.ScaleControl(factor, specified);
    }

    /// <summary>
    ///  Gets or sets the foreground color of the scroll bar control.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override Color ForeColor
    {
        get => base.ForeColor;
        set => base.ForeColor = value;
    }

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

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [AllowNull]
    public override Font Font
    {
        get => base.Font;
        set => base.Font = value;
    }

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

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

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

    /// <summary>
    ///  Gets or sets a value to be added or subtracted to the <see cref="Value"/>
    ///  property when the scroll box is moved a large distance.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(10)]
    [SRDescription(nameof(SR.ScrollBarLargeChangeDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    public int LargeChange
    {
        get
        {
            // We preserve the actual large change value that has been set, but when we come to
            // get the value of this property, make sure it's within the maximum allowable value.
            // This way we ensure that we don't depend on the order of property sets when
            // code is generated at design-time.
            return Math.Min(_largeChange, _maximum - _minimum + 1);
        }
        set
        {
            if (_largeChange != value)
            {
                ArgumentOutOfRangeException.ThrowIfNegative(value);

                _largeChange = value;
                UpdateScrollInfo();
            }
        }
    }

    /// <summary>
    ///  Gets or sets the upper limit of values of the scrollable range.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(100)]
    [SRDescription(nameof(SR.ScrollBarMaximumDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    public int Maximum
    {
        get => _maximum;
        set
        {
            if (_maximum != value)
            {
                if (_minimum > value)
                {
                    _minimum = value;
                }

                if (value < _value)
                {
                    Value = value;
                }

                _maximum = value;
                UpdateScrollInfo();
            }
        }
    }

    /// <summary>
    ///  Gets or sets the lower limit of values of the scrollable range.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(0)]
    [SRDescription(nameof(SR.ScrollBarMinimumDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    public int Minimum
    {
        get => _minimum;
        set
        {
            if (_minimum != value)
            {
                if (_maximum < value)
                {
                    _maximum = value;
                }

                if (value > _value)
                {
                    _value = value;
                }

                _minimum = value;
                UpdateScrollInfo();
            }
        }
    }

    /// <summary>
    ///  Gets or sets the value to be added or subtracted to the <see cref="Value"/>
    ///  property when the scroll box is moved a small distance.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(1)]
    [SRDescription(nameof(SR.ScrollBarSmallChangeDescr))]
    public int SmallChange
    {
        get
        {
            // We can't have SmallChange > LargeChange, but we shouldn't manipulate
            // the set values for these properties, so we just return the smaller
            // value here.
            return Math.Min(_smallChange, LargeChange);
        }
        set
        {
            if (_smallChange != value)
            {
                ArgumentOutOfRangeException.ThrowIfNegative(value);

                _smallChange = value;
                UpdateScrollInfo();
            }
        }
    }

    internal override bool SupportsUiaProviders => true;

    [DefaultValue(false)]
    public new bool TabStop
    {
        get => base.TabStop;
        set => base.TabStop = value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Bindable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [AllowNull]
    public override string Text
    {
        get => base.Text;
        set => base.Text = value;
    }

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

    /// <summary>
    ///  Gets or sets a numeric value that represents the current position of the scroll box
    ///  on the scroll bar control.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(0)]
    [Bindable(true)]
    [SRDescription(nameof(SR.ScrollBarValueDescr))]
    public int Value
    {
        get => _value;
        set
        {
            if (_value != value)
            {
                if (value < _minimum || value > _maximum)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), string.Format(SR.InvalidBoundArgument, nameof(Value), value, $"'{nameof(Minimum)}'", $"'{nameof(Maximum)}'"));
                }

                int oldValue = _value;
                _value = value;
                UpdateScrollInfo();
                OnValueChanged(EventArgs.Empty);
                if (IsAccessibilityObjectCreated)
                {
                    AccessibilityObject.RaiseAutomationPropertyChangedEvent(UIA_PROPERTY_ID.UIA_RangeValueValuePropertyId, (VARIANT)(double)oldValue, (VARIANT)(double)_value);
                }
            }
        }
    }

    /// <summary>
    ///  Get/Set flag to let scrollbar scale according to the DPI of the window.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [Browsable(true)]
    [EditorBrowsable(EditorBrowsableState.Always)]
    [SRDescription(nameof(SR.ControlDpiChangeScale))]
    public bool ScaleScrollBarForDpiChange { get; set; } = true;

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

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

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

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

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

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

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

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

    /// <summary>
    ///  Occurs when the scroll box has been moved by either a mouse or keyboard action.
    /// </summary>
    [SRCategory(nameof(SR.CatAction))]
    [SRDescription(nameof(SR.ScrollBarOnScrollDescr))]
    public event ScrollEventHandler? Scroll
    {
        add => Events.AddHandler(s_scrollEvent, value);
        remove => Events.RemoveHandler(s_scrollEvent, value);
    }

    /// <summary>
    ///  Occurs when the <see cref="Value"/> property has
    ///  changed, either by a <see cref="OnScroll"/> event
    ///  or programmatically.
    /// </summary>
    [SRCategory(nameof(SR.CatAction))]
    [SRDescription(nameof(SR.valueChangedEventDescr))]
    public event EventHandler? ValueChanged
    {
        add => Events.AddHandler(s_valueChangedEvent, value);
        remove => Events.RemoveHandler(s_valueChangedEvent, value);
    }

    /// <summary>
    ///  This is a helper method that is called by ScaleControl to retrieve the bounds
    ///  that the control should be scaled by. You may override this method if you
    ///  wish to reuse ScaleControl's scaling logic but you need to supply your own
    ///  bounds. The default implementation returns scaled bounds that take into
    ///  account the BoundsSpecified, whether the control is top level, and whether
    ///  the control is fixed width or auto size, and any adornments the control may have.
    /// </summary>
    protected override Rectangle GetScaledBounds(Rectangle bounds, SizeF factor, BoundsSpecified specified)
    {
        // Adjust Specified for vertical or horizontal scaling
        if (_scrollOrientation == ScrollOrientation.VerticalScroll)
        {
            specified &= ~BoundsSpecified.Width;
        }
        else
        {
            specified &= ~BoundsSpecified.Height;
        }

        return base.GetScaledBounds(bounds, factor, specified);
    }

    internal override HBRUSH InitializeDCForWmCtlColor(HDC dc, MessageId msg) => default;

    protected override void OnEnabledChanged(EventArgs e)
    {
        if (Enabled)
        {
            UpdateScrollInfo();
        }

        base.OnEnabledChanged(e);
    }

    /// <summary>
    ///  Creates the handle. overridden to help set up scrollbar information.
    /// </summary>
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        UpdateScrollInfo();
    }

    /// <summary>
    ///  Raises the <see cref="ValueChanged"/> event.
    /// </summary>
    protected virtual void OnScroll(ScrollEventArgs se)
        => ((ScrollEventHandler?)Events[s_scrollEvent])?.Invoke(this, se);

    /// <summary>
    ///  Converts mouse wheel movements into scrolling, when scrollbar has the focus.
    ///  Typically one wheel step will cause one small scroll increment, in either
    ///  direction. A single wheel message could represent either one wheel step, multiple
    ///  wheel steps (fast wheeling), or even a fraction of a step (smooth-wheeled mice).
    ///  So we accumulate the total wheel delta, and consume it in whole numbers of steps.
    /// </summary>
    protected override void OnMouseWheel(MouseEventArgs e)
    {
        if (e is not null)
        {
            _wheelDelta += e.Delta;

            bool scrolled = false;

            while (Math.Abs(_wheelDelta) >= PInvoke.WHEEL_DELTA)
            {
                if (_wheelDelta > 0)
                {
                    _wheelDelta -= (int)PInvoke.WHEEL_DELTA;
                    DoScroll(ScrollEventType.SmallDecrement);
                    scrolled = true;
                }
                else
                {
                    _wheelDelta += (int)PInvoke.WHEEL_DELTA;
                    DoScroll(ScrollEventType.SmallIncrement);
                    scrolled = true;
                }
            }

            if (scrolled)
            {
                DoScroll(ScrollEventType.EndScroll);
            }

            if (e is HandledMouseEventArgs mouseEventArgs)
            {
                mouseEventArgs.Handled = true;
            }
        }

        // The base implementation should be called before the implementation above,
        // but changing the order in Whidbey would be too much of a breaking change
        // for this particular class.
        // Because the code has been like that since long time, we assume that e is not null.
        base.OnMouseWheel(e!);
    }

    /// <summary>
    ///  Raises the <see cref="ValueChanged"/> event.
    /// </summary>
    protected virtual void OnValueChanged(EventArgs e)
        => ((EventHandler?)Events[s_valueChangedEvent])?.Invoke(this, e);

    private int ReflectPosition(int position)
    {
        if (this is HScrollBar)
        {
            return _minimum + (_maximum - LargeChange + 1) - position;
        }

        return position;
    }

    public override string ToString()
    {
        string s = base.ToString();
        return $"{s}, Minimum: {Minimum}, Maximum: {Maximum}, Value: {Value}";
    }

    protected unsafe void UpdateScrollInfo()
    {
        if (!IsHandleCreated || !Enabled)
        {
            return;
        }

        SCROLLINFO si = new()
        {
            cbSize = (uint)sizeof(SCROLLINFO),
            fMask = SCROLLINFO_MASK.SIF_ALL,
            nMin = _minimum,
            nMax = _maximum,
            nPage = (uint)LargeChange
        };

        if (RightToLeft == RightToLeft.Yes)
        {
            // Reflect the scrollbar position horizontally on an Rtl system
            si.nPos = ReflectPosition(_value);
        }
        else
        {
            si.nPos = _value;
        }

        si.nTrackPos = 0;
        PInvoke.SetScrollInfo(this, SCROLLBAR_CONSTANTS.SB_CTL, ref si, true);
    }

    private void WmReflectScroll(ref Message m)
    {
        ScrollEventType type = (ScrollEventType)m.WParamInternal.LOWORD;
        DoScroll(type);
    }

    private unsafe void DoScroll(ScrollEventType type)
    {
        // For Rtl systems we need to swap increment and decrement
        if (RightToLeft == RightToLeft.Yes)
        {
            switch (type)
            {
                case ScrollEventType.First:
                    type = ScrollEventType.Last;
                    break;

                case ScrollEventType.Last:
                    type = ScrollEventType.First;
                    break;

                case ScrollEventType.SmallDecrement:
                    type = ScrollEventType.SmallIncrement;
                    break;

                case ScrollEventType.SmallIncrement:
                    type = ScrollEventType.SmallDecrement;
                    break;

                case ScrollEventType.LargeDecrement:
                    type = ScrollEventType.LargeIncrement;
                    break;

                case ScrollEventType.LargeIncrement:
                    type = ScrollEventType.LargeDecrement;
                    break;
            }
        }

        int newValue = _value;
        int oldValue = _value;

        // The ScrollEventArgs constants are defined in terms of the windows
        // messages. This eliminates confusion between the VSCROLL and
        // HSCROLL constants, which are identical.
        switch (type)
        {
            case ScrollEventType.First:
                newValue = _minimum;
                break;

            case ScrollEventType.Last:
                newValue = _maximum - LargeChange + 1;
                break;

            case ScrollEventType.SmallDecrement:
                newValue = Math.Max(_value - SmallChange, _minimum);
                break;

            case ScrollEventType.SmallIncrement:
                newValue = Math.Min(_value + SmallChange, _maximum - LargeChange + 1);
                break;

            case ScrollEventType.LargeDecrement:
                newValue = Math.Max(_value - LargeChange, _minimum);
                break;

            case ScrollEventType.LargeIncrement:
                newValue = Math.Min(_value + LargeChange, _maximum - LargeChange + 1);
                break;

            case ScrollEventType.ThumbPosition:
            case ScrollEventType.ThumbTrack:
                SCROLLINFO si = new()
                {
                    cbSize = (uint)sizeof(SCROLLINFO),
                    fMask = SCROLLINFO_MASK.SIF_TRACKPOS
                };

                PInvoke.GetScrollInfo(this, SCROLLBAR_CONSTANTS.SB_CTL, ref si);

                if (RightToLeft == RightToLeft.Yes)
                {
                    newValue = ReflectPosition(si.nTrackPos);
                }
                else
                {
                    newValue = si.nTrackPos;
                }

                break;
        }

        ScrollEventArgs se = new(type, oldValue, newValue, _scrollOrientation);
        OnScroll(se);
        Value = se.NewValue;
    }

    protected override void WndProc(ref Message m)
    {
        switch (m.MsgInternal)
        {
            case MessageId.WM_REFLECT_HSCROLL:
            case MessageId.WM_REFLECT_VSCROLL:
                WmReflectScroll(ref m);
                break;

            case PInvoke.WM_ERASEBKGND:
                break;

            case PInvoke.WM_SIZE:
                // Fixes the scrollbar focus rect
                if (PInvoke.GetFocus() == HWND)
                {
                    DefWndProc(ref m);
                    PInvoke.SendMessage(this, PInvoke.WM_KILLFOCUS);
                    PInvoke.SendMessage(this, PInvoke.WM_SETFOCUS);
                }

                break;

            default:
                base.WndProc(ref m);
                break;
        }
    }

    /// <summary>
    ///  Creates a new AccessibleObject for this <see cref="ScrollBar"/> instance.
    ///  The AccessibleObject instance returned by this method supports ControlType UIA property.
    /// </summary>
    /// <returns>
    ///  AccessibleObject for this <see cref="ScrollBar"/> instance.
    /// </returns>
    protected override AccessibleObject CreateAccessibilityInstance()
        => new ScrollBarAccessibleObject(this);
}
