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

namespace System.Windows.Forms;

/// <summary>
///  Encapsulates a standard Windows radio button (option button).
/// </summary>
[DefaultProperty(nameof(Checked))]
[DefaultEvent(nameof(CheckedChanged))]
[DefaultBindingProperty(nameof(Checked))]
[ToolboxItem($"System.Windows.Forms.Design.AutoSizeToolboxItem,{Assemblies.SystemDesign}")]
[Designer($"System.Windows.Forms.Design.RadioButtonDesigner, {Assemblies.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionRadioButton))]
public partial class RadioButton : ButtonBase
{
    private static readonly object s_checkedChangedEvent = new();
    private static readonly object s_appearanceChangeEvent = new();

    private const ContentAlignment AnyRight = ContentAlignment.TopRight | ContentAlignment.MiddleRight | ContentAlignment.BottomRight;

    // Used to see if we need to iterate through the auto checked items and modify their tab stops.
    private bool _firstFocus = true;
    private bool _isChecked;
    private bool _autoCheck = true;
    private ContentAlignment _checkAlign = ContentAlignment.MiddleLeft;
    private Appearance _appearance = Appearance.Normal;
    private Rendering.RadioButton.AnimatedRadioGlyphRenderer? _radioGlyphRenderer;
    private Rendering.CheckBox.AnimatedToggleSwitchRenderer? _toggleSwitchRenderer;
    private int _flatSystemStylePaddingWidth;
    private int _flatSystemStyleMinimumHeight;

    /// <summary>
    ///  Initializes a new instance of the <see cref="RadioButton"/> class.
    /// </summary>
    public RadioButton() : base()
    {
        ScaleConstants();

        // Radio buttons shouldn't respond to right clicks, so we need to do all our own click logic
        SetStyle(ControlStyles.StandardClick, false);

        TextAlign = ContentAlignment.MiddleLeft;
        TabStop = false;
        SetAutoSizeMode(AutoSizeMode.GrowAndShrink);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the <see cref="Checked"/> value and the appearance of
    ///  the control automatically change when the control is clicked.
    /// </summary>
    [DefaultValue(true)]
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RadioButtonAutoCheckDescr))]
    public bool AutoCheck
    {
        get => _autoCheck;
        set
        {
            if (_autoCheck != value)
            {
                _autoCheck = value;
                PerformAutoUpdates(false);
            }
        }
    }

    /// <summary>
    ///  Gets or sets the value that determines the appearance of the radio button.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   <see cref="Appearance.ToggleSwitch"/> selects the framework's owner-drawn toggle renderer in an
    ///   effective .NET 11-or-later mode. High Contrast resolves to classic rendering. Modern toggle metrics can
    ///   increase preferred size; see the
    ///   <see href="https://github.com/dotnet/winforms/blob/main/docs/net11-visualstyles-layout-guidance.md">
    ///   .NET 11 VisualStyles layout guidance</see>.
    ///  </para>
    /// </remarks>
    [DefaultValue(Appearance.Normal)]
    [SRCategory(nameof(SR.CatAppearance))]
    [Localizable(true)]
    [SRDescription(nameof(SR.RadioButtonAppearanceDescr))]
    public Appearance Appearance
    {
        get => _appearance;
        set
        {
            if (_appearance != value)
            {
                SourceGenerated.EnumValidator.Validate(value);

                using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.Appearance))
                {
                    _appearance = value;
                    ResetAdapter();

                    // UpdateOwnerDraw checks if OwnerDraw state changed and calls RecreateHandle if needed.
                    UpdateOwnerDraw();
                    UpdateToggleSwitchStyles();

                    // If handle wasn't recreated (OwnerDraw state didn't change), refresh the appearance.
                    if (OwnerDraw)
                    {
                        Refresh();
                    }
                    else
                    {
                        UpdateStyles();
                    }

                    OnAppearanceChanged(EventArgs.Empty);
                }
            }
        }
    }

    [SRCategory(nameof(SR.CatPropertyChanged))]
    [SRDescription(nameof(SR.RadioButtonOnAppearanceChangedDescr))]
    public event EventHandler? AppearanceChanged
    {
        add => Events.AddHandler(s_appearanceChangeEvent, value);
        remove => Events.RemoveHandler(s_appearanceChangeEvent, value);
    }

    /// <summary>
    ///  Gets or sets the location of the check box portion of the radio button control.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   When <see cref="Appearance"/> is <see cref="Appearance.ToggleSwitch"/> and the control renders as a toggle
    ///   switch in an effective .NET 11-or-later mode,
    ///   <see cref="ContentAlignment.TopLeft"/>, <see cref="ContentAlignment.MiddleLeft"/>,
    ///   <see cref="ContentAlignment.BottomLeft"/>, <see cref="ContentAlignment.TopCenter"/>,
    ///   <see cref="ContentAlignment.MiddleCenter"/>, and <see cref="ContentAlignment.BottomCenter"/> are treated
    ///   like left-aligned values, and
    ///   <see cref="ContentAlignment.TopRight"/>, <see cref="ContentAlignment.MiddleRight"/>, and
    ///   <see cref="ContentAlignment.BottomRight"/> are treated like right-aligned values.
    ///  </para>
    /// </remarks>
    [Localizable(true)]
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(ContentAlignment.MiddleLeft)]
    [SRDescription(nameof(SR.RadioButtonCheckAlignDescr))]
    public ContentAlignment CheckAlign
    {
        get => _checkAlign;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            _checkAlign = value;
            if (OwnerDraw)
            {
                Invalidate();
            }
            else
            {
                UpdateStyles();
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the control is checked or not.
    /// </summary>
    [Bindable(true),
    SettingsBindable(true)]
    [DefaultValue(false)]
    [SRCategory(nameof(SR.CatAppearance))]
    [SRDescription(nameof(SR.RadioButtonCheckedDescr))]
    public bool Checked
    {
        get => _isChecked;
        set
        {
            if (_isChecked != value)
            {
                bool animateToggleSwitch = IsToggleSwitchAppearance && IsHandleCreated;
                if (animateToggleSwitch)
                {
                    ToggleSwitchRenderer.PrepareStateChange();
                }

                _isChecked = value;

                if (IsHandleCreated)
                {
                    PInvokeCore.SendMessage(this, PInvoke.BM_SETCHECK, (WPARAM)(BOOL)value);
                }

                Invalidate();
                Update();
                PerformAutoUpdates(tabbedInto: false);
                OnCheckedChanged(EventArgs.Empty);

                if (animateToggleSwitch)
                {
                    ToggleSwitchRenderer.StartAnimation();
                }
            }
        }
    }

    private protected override bool OwnerDraw =>
        // Order is key here - do NOT change!
        // We want NO owner draw ONLY when we're
        // * in Dark Mode
        // * when _then_ the Appearance is Button
        // * but then ONLY when we're rendering with FlatStyle.Standard
        //   (because that would let us usually let us draw with the VisualStyleRenderers,
        //   which cause HighDPI issues in Dark Mode).
        IsToggleSwitchAppearance
            || ((!Application.IsDarkModeEnabled
                || Appearance != Appearance.Button
                || FlatStyle != FlatStyle.Standard)
                && base.OwnerDraw);

    private bool IsToggleSwitchAppearance
        => Appearance == Appearance.ToggleSwitch
            && EffectiveVisualStylesMode >= VisualStylesMode.Net11;

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

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

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ClassName = PInvoke.WC_BUTTON;
            if (OwnerDraw)
            {
                cp.Style |= PInvoke.BS_OWNERDRAW;
            }
            else
            {
                cp.Style |= PInvoke.BS_RADIOBUTTON;
                if (Appearance == Appearance.Button)
                {
                    cp.Style |= PInvoke.BS_PUSHLIKE;
                }

                // Determine the alignment of the radio button
                ContentAlignment align = RtlTranslateContent(CheckAlign);
                if ((align & AnyRight) != 0)
                {
                    cp.Style |= PInvoke.BS_RIGHTBUTTON;
                }
            }

            return cp;
        }
    }

    protected override Size DefaultSize => new(104, 24);

    /// <summary>
    ///  When overridden in a derived class, handles rescaling of any magic numbers used in control painting.
    ///  For RadioButton controls, scale the width of the system-style padding and height of the radio button image.
    ///  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 and EnableDpiChangedHighDpiImprovements config switches turned on.
    /// </summary>
    /// <param name="deviceDpiOld">Old DPI value</param>
    /// <param name="deviceDpiNew">New DPI value</param>
    protected override void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew)
    {
        base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew);
        ScaleConstants();
    }

    private void ScaleConstants()
    {
        const int LogicalFlatSystemStylePaddingWidth = 24;
        const int LogicalFlatSystemStyleMinimumHeight = 13;
        _flatSystemStylePaddingWidth = LogicalToDeviceUnits(LogicalFlatSystemStylePaddingWidth);
        _flatSystemStyleMinimumHeight = LogicalToDeviceUnits(LogicalFlatSystemStyleMinimumHeight);
    }

    private protected override void InitializeControl() => ScaleConstants();

    internal override Size GetPreferredSizeCore(Size proposedConstraints)
    {
        if (IsToggleSwitchAppearance)
        {
            return Rendering.CheckBox.ToggleSwitchMetrics.Create(this).GetPreferredSize(this);
        }

        if (Appearance == Appearance.Button && FlatStyle == FlatStyle.Popup)
        {
            return DarkModeAdapterFactory.CreatePopupAdapter(this).GetPreferredSizeCore(proposedConstraints)
                + Padding.Size;
        }

        if (FlatStyle != FlatStyle.System)
        {
            return base.GetPreferredSizeCore(proposedConstraints);
        }

        Size textSize = TextRenderer.MeasureText(Text, Font);
        Size size = SizeFromClientSize(textSize);
        size.Width += _flatSystemStylePaddingWidth;

        // Ensure minimum height to avoid truncation of RadioButton circle or text.
        size.Height = ScaleHelper.IsScalingRequirementMet
            ? Math.Max(size.Height + 5, _flatSystemStyleMinimumHeight)
            : size.Height + 5;

        return size;
    }

    internal override Rectangle OverChangeRectangle
    {
        get
        {
            if (Appearance == Appearance.Button)
            {
                return base.OverChangeRectangle;
            }
            else if (FlatStyle == FlatStyle.Standard)
            {
                // Return an out of bounds rectangle to avoid invalidation.
                return new Rectangle(-1, -1, 1, 1);
            }
            else
            {
                return Adapter.CommonLayout().Layout().CheckBounds;
            }
        }
    }

    internal override Rectangle DownChangeRectangle =>
        Appearance == Appearance.Button || FlatStyle == FlatStyle.System
                ? base.DownChangeRectangle
                : Adapter.CommonLayout().Layout().CheckBounds;

    internal override bool SupportsUiaProviders => true;

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

    /// <summary>
    ///  Gets or sets the value indicating whether the user can give the focus to this
    ///  control using the TAB key.
    /// </summary>
    [Localizable(true)]
    [DefaultValue(ContentAlignment.MiddleLeft)]
    public override ContentAlignment TextAlign
    {
        get => base.TextAlign;
        set => base.TextAlign = value;
    }

    /// <summary>
    ///  Occurs when the
    ///  value of the <see cref="Checked"/>
    ///  property changes.
    /// </summary>
    [SRDescription(nameof(SR.RadioButtonOnCheckedChangedDescr))]
    public event EventHandler? CheckedChanged
    {
        add => Events.AddHandler(s_checkedChangedEvent, value);
        remove => Events.RemoveHandler(s_checkedChangedEvent, value);
    }

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

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);

        if (IsHandleCreated)
        {
            PInvokeCore.SendMessage(this, PInvoke.BM_SETCHECK, (WPARAM)(BOOL)_isChecked);
        }

        UpdateToggleSwitchStyles();
    }

    /// <inheritdoc/>
    protected override void OnPaint(PaintEventArgs pevent)
    {
        if (IsToggleSwitchAppearance)
        {
            using GraphicsStateScope scope = new(pevent.Graphics);
            ToggleSwitchRenderer.RenderControl(pevent.Graphics);
            return;
        }

        base.OnPaint(pevent);
    }

    /// <inheritdoc/>
    protected override void OnVisualStylesModeChanged(EventArgs e)
    {
        base.OnVisualStylesModeChanged(e);
        UpdateOwnerDraw();
        UpdateToggleSwitchStyles();

        if (IsHandleCreated)
        {
            Invalidate();
        }
    }

    /// <inheritdoc/>
    protected override void OnSystemColorsChanged(EventArgs e)
    {
        base.OnSystemColorsChanged(e);
        _radioGlyphRenderer?.InvalidateAccentColor();
        _toggleSwitchRenderer?.InvalidateAccentColor();
        UpdateOwnerDraw();
        UpdateToggleSwitchStyles();
    }

    /// <summary>
    ///  Raises the <see cref="CheckBox.CheckedChanged"/> event.
    /// </summary>
    protected virtual void OnCheckedChanged(EventArgs e)
    {
        // MSAA events:
        AccessibilityNotifyClients(AccessibleEvents.StateChange, -1);
        AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);

        // UIA events:
        if (IsAccessibilityObjectCreated)
        {
            using var nameVariant = (VARIANT)Name;
            AccessibilityObject.RaiseAutomationPropertyChangedEvent(UIA_PROPERTY_ID.UIA_NamePropertyId, nameVariant, nameVariant);
            AccessibilityObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationPropertyChangedEventId);
        }

        ((EventHandler?)Events[s_checkedChangedEvent])?.Invoke(this, e);
    }

    protected override void OnClick(EventArgs e)
    {
        if (_autoCheck)
        {
            Checked = true;
        }

        base.OnClick(e);
    }

    protected internal override void OnEnter(EventArgs e)
    {
        // Just like the Win32 RadioButton, fire a click if the user arrows onto the control.
        if (MouseButtons == MouseButtons.None)
        {
            if (PInvoke.GetKeyState((int)Keys.Tab) >= 0)
            {
                // Entered the radio button by using arrow keys.
                // Paint in raised state.
                ResetFlagsandPaint();
                if (!ValidationCancelled)
                {
                    OnClick(e);
                }
            }
            else
            {
                // Entered the radio button by pressing Tab
                PerformAutoUpdates(true);

                // Reset the TabStop so we can come back later. PerformAutoUpdates will set TabStop to false.
                TabStop = true;
            }
        }

        base.OnEnter(e);
    }

    private void PerformAutoUpdates(bool tabbedInto)
    {
        if (!_autoCheck)
        {
            return;
        }

        if (_firstFocus)
        {
            WipeTabStops(tabbedInto);
        }

        TabStop = _isChecked;
        if (!_isChecked || ParentInternal is not { } parent)
        {
            return;
        }

        ControlCollection children = parent.Controls;
        for (int i = 0; i < children.Count; i++)
        {
            if (children[i] is RadioButton radioButton
                && radioButton != this
                && radioButton.AutoCheck
                && radioButton.Checked)
            {
                radioButton.Checked = false;
            }
        }
    }

    /// <summary>
    ///  Removes tab stops from all radio buttons, other than the one that currently has the focus.
    /// </summary>
    private void WipeTabStops(bool tabbedInto)
    {
        if (ParentInternal is not { } parent)
        {
            return;
        }

        ControlCollection children = parent.Controls;
        for (int i = 0; i < children.Count; i++)
        {
            if (children[i] is RadioButton button)
            {
                if (!tabbedInto)
                {
                    button._firstFocus = false;
                }

                if (button._autoCheck)
                {
                    button.TabStop = false;
                }
            }
        }
    }

    internal override ButtonBaseAdapter CreateFlatAdapter()
        => UseModernGlyphRenderer
            ? new RadioButtonModernAdapter(this, FlatStyle.Flat)
            : new RadioButtonFlatAdapter(this);

    internal override ButtonBaseAdapter CreatePopupAdapter()
        => UseModernGlyphRenderer
            ? new RadioButtonModernAdapter(this, FlatStyle.Popup)
            : new RadioButtonPopupAdapter(this);

    internal override ButtonBaseAdapter CreateStandardAdapter()
        => UseModernGlyphRenderer
            ? new RadioButtonModernAdapter(this, FlatStyle.Standard)
            : new RadioButtonStandardAdapter(this);

    internal Rendering.RadioButton.AnimatedRadioGlyphRenderer RadioGlyphRenderer
        => _radioGlyphRenderer ??= new(this);

    internal Rendering.CheckBox.AnimatedToggleSwitchRenderer ToggleSwitchRenderer =>
        _toggleSwitchRenderer ??= new(this, Rendering.CheckBox.ModernCheckBoxStyle.Rounded);

    private void UpdateToggleSwitchStyles()
    {
        if (IsToggleSwitchAppearance)
        {
            SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
            _toggleSwitchRenderer?.SynchronizeState();
        }
        else
        {
            _toggleSwitchRenderer?.StopAnimation();
        }
    }

    private bool UseModernGlyphRenderer
        => Appearance == Appearance.Normal
            && EffectiveVisualStylesMode >= VisualStylesMode.Net11;

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _radioGlyphRenderer?.Dispose();
            _radioGlyphRenderer = null;
            _toggleSwitchRenderer?.Dispose();
            _toggleSwitchRenderer = null;
        }

        base.Dispose(disposing);
    }

    private void OnAppearanceChanged(EventArgs e)
    {
        if (Events[s_appearanceChangeEvent] is EventHandler eh)
        {
            eh(this, e);
        }
    }

    internal ContentAlignment RtlTranslatedCheckAlign
        => RtlTranslateContent(CheckAlign);

    internal bool ShowFocusCuesInternal => ShowFocusCues;

    protected override void OnMouseUp(MouseEventArgs mevent)
    {
        if (mevent.Button == MouseButtons.Left
            && GetStyle(ControlStyles.UserPaint)
            && MouseIsDown
            && PInvoke.WindowFromPoint(PointToScreen(mevent.Location)) == HWND)
        {
            // Paint in raised state.
            ResetFlagsandPaint();
            if (!ValidationCancelled)
            {
                OnClick(mevent);
                OnMouseClick(mevent);
            }
        }

        base.OnMouseUp(mevent);
    }

    /// <summary>
    ///  Generates a <see cref="Control.Click"/> event for the button, simulating a click by a user.
    /// </summary>
    public void PerformClick()
    {
        if (CanSelect)
        {
            // Paint in raised state.
            ResetFlagsandPaint();
            if (!ValidationCancelled)
            {
                OnClick(EventArgs.Empty);
            }
        }
    }

    protected internal override bool ProcessMnemonic(char charCode)
    {
        if (UseMnemonic && IsMnemonic(charCode, Text) && CanSelect)
        {
            if (!Focused)
            {
                // This will cause an OnEnter event, which in turn will fire the click event.
                Focus();
            }
            else
            {
                // Generate a click if already focused.
                PerformClick();
            }

            return true;
        }

        return false;
    }

    public override string ToString() => $"{base.ToString()}, Checked: {Checked}";
}
