// 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>
///  Represents a Windows check box.
/// </summary>
[DefaultProperty(nameof(Checked))]
[DefaultEvent(nameof(CheckedChanged))]
[DefaultBindingProperty(nameof(CheckState))]
[ToolboxItem($"System.Windows.Forms.Design.AutoSizeToolboxItem,{Assemblies.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionCheckBox))]
public partial class CheckBox : ButtonBase
{
    private static readonly object s_checkedChangedEvent = new();
    private static readonly object s_checkStateChangedEvent = new();
    private static readonly object s_appearanceChangedEvent = new();
    private const ContentAlignment AnyRight = ContentAlignment.TopRight | ContentAlignment.MiddleRight | ContentAlignment.BottomRight;

    private ContentAlignment _checkAlign = ContentAlignment.MiddleLeft;
    private CheckState _checkState;
    private Appearance _appearance;
    private bool _threeState;

    private Rendering.CheckBox.AnimatedCheckGlyphRenderer? _checkGlyphRenderer;
    private Rendering.CheckBox.AnimatedToggleSwitchRenderer? _toggleSwitchRenderer;

    private int _flatSystemStylePaddingWidth;
    private int _flatSystemStyleMinimumHeight;

    // A flag indicating if UIA StateChanged event needs to be triggered,
    // to avoid double-triggering when Checked value changes.
    private bool _notifyAccessibilityStateChangedNeeded;

    /// <summary>
    ///  Initializes a new instance of the <see cref="CheckBox"/> class.
    /// </summary>
    public CheckBox() : base()
    {
        // Checkboxes shouldn't respond to right clicks, so we need to do all our own click logic
        SetStyle(ControlStyles.StandardClick | ControlStyles.StandardDoubleClick, false);
        SetAutoSizeMode(AutoSizeMode.GrowAndShrink);

        AutoCheck = true;
        TextAlign = ContentAlignment.MiddleLeft;
    }

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

    private bool AccObjDoDefaultAction { get; set; }

    public partial Appearance Appearance
    {
        get => _appearance;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            if (_appearance == value)
            {
                return;
            }

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

                // UpdateOwnerDraw synchronizes control styles with the OwnerDraw state and recreates
                // the handle if they differ. Since we hijack FlatStyle.Standard for DarkMode, the transition
                // between Normal and Button appearance is critical for updating the OwnerDraw flag.
                UpdateOwnerDraw();
                UpdateToggleSwitchStyles();

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

                OnAppearanceChanged(EventArgs.Empty);
            }
        }
    }

    private protected override bool OwnerDraw =>
            // Appearance.ToggleSwitch intentionally wins over FlatStyle.System: a toggle has no
            // native BS_GROUPBOX rendering and must remain owner-drawn so the requested appearance
            // is never ignored and mouse-state geometry is always available.
            IsToggleSwitchAppearance
            ||
            // 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).
            ((!Application.IsDarkModeEnabled
                || Appearance != Appearance.Button
                || FlatStyle != FlatStyle.Standard)
                && base.OwnerDraw);

    /// <summary>
    ///  Gets a value indicating whether the check box should render as the modern, animated toggle switch.
    /// </summary>
    private bool IsToggleSwitchAppearance
    {
        get
        {
            return Appearance == Appearance.ToggleSwitch
                && EffectiveVisualStylesMode >= VisualStylesMode.Net11
                && !ThreeState;
        }
    }

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

    internal Rendering.CheckBox.AnimatedCheckGlyphRenderer CheckGlyphRenderer
        => _checkGlyphRenderer ??= new(this);

    private void UpdateToggleSwitchStyles()
    {
        if (IsToggleSwitchAppearance)
        {
            // Owner-paint with WinForms double buffering for a flicker-free, fluent animation.
            SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
            _toggleSwitchRenderer?.SynchronizeState();
        }
        else
        {
            _toggleSwitchRenderer?.StopAnimation();
        }
    }

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

    /// <summary>
    ///  Gets or sets a value indicating whether the <see cref="Checked"/> or <see cref="CheckState"/>
    ///  value and the check box's appearance are automatically changed when it is clicked.
    /// </summary>
    [DefaultValue(true)]
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.CheckBoxAutoCheckDescr))]
    public bool AutoCheck { get; set; }

    /// <summary>
    ///  Gets or sets the horizontal and vertical alignment of a check box on a check box control.
    /// </summary>
    [Bindable(true)]
    [Localizable(true)]
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(ContentAlignment.MiddleLeft)]
    [SRDescription(nameof(SR.CheckBoxCheckAlignDescr))]
    public ContentAlignment CheckAlign
    {
        get => _checkAlign;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            if (_checkAlign == value)
            {
                return;
            }

            _checkAlign = value;
            LayoutTransaction.DoLayoutIf(AutoSize, ParentInternal, this, PropertyNames.CheckAlign);
            if (OwnerDraw)
            {
                Invalidate();
            }
            else
            {
                UpdateStyles();
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the check box is checked.
    /// </summary>
    [Bindable(true),
    SettingsBindable(true)]
    [DefaultValue(false)]
    [SRCategory(nameof(SR.CatAppearance))]
    [RefreshProperties(RefreshProperties.All)]
    [SRDescription(nameof(SR.CheckBoxCheckedDescr))]
    public bool Checked
    {
        get => _checkState != CheckState.Unchecked;
        set
        {
            if (value != Checked)
            {
                CheckState = value ? CheckState.Checked : CheckState.Unchecked;
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the check box is checked.
    /// </summary>
    [Bindable(true)]
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(CheckState.Unchecked)]
    [RefreshProperties(RefreshProperties.All)]
    [SRDescription(nameof(SR.CheckBoxCheckStateDescr))]
    public CheckState CheckState
    {
        get => _checkState;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            if (_checkState == value)
            {
                return;
            }

            // For the animated toggle switch, stop any in-flight animation (leaving the thumb at its current
            // position) before the state changes, then start a fresh animation toward the new position.
            bool animateToggleSwitch = IsToggleSwitchAppearance && IsHandleCreated;
            if (animateToggleSwitch)
            {
                ToggleSwitchRenderer.PrepareStateChange();
            }

            bool oldChecked = Checked;

            _checkState = value;

            if (IsHandleCreated)
            {
                PInvokeCore.SendMessage(this, PInvoke.BM_SETCHECK, (WPARAM)(int)_checkState);
            }

            bool checkedChanged = oldChecked != Checked;

            if (checkedChanged)
            {
                OnCheckedChanged(EventArgs.Empty);
            }

            _notifyAccessibilityStateChangedNeeded = !checkedChanged;
            OnCheckStateChanged(EventArgs.Empty);
            _notifyAccessibilityStateChangedNeeded = false;

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

    /// <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_3STATE;
                if (Appearance == Appearance.Button)
                {
                    cp.Style |= PInvoke.BS_PUSHLIKE;
                }

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

            return cp;
        }
    }

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

    protected override void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew)
    {
        base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew);
        ScaleConstants();
    }

    private void ScaleConstants()
    {
        const int LogicalFlatSystemStylePaddingWidth = 25;
        const int LogicalFlatSystemStyleMinimumHeight = 13;

        _flatSystemStylePaddingWidth = LogicalToDeviceUnits(LogicalFlatSystemStylePaddingWidth);
        _flatSystemStyleMinimumHeight = LogicalToDeviceUnits(LogicalFlatSystemStyleMinimumHeight);
    }

    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 (Appearance == Appearance.Button)
        {
            if (EffectiveVisualStylesModeInternal >= VisualStylesMode.Net11
                && FlatStyle is FlatStyle.Standard or FlatStyle.Flat)
            {
                ButtonBaseAdapter modernAdapter = FlatStyle == FlatStyle.Flat
                    ? DarkModeAdapterFactory.CreateFlatAdapter(this)
                    : DarkModeAdapterFactory.CreateStandardAdapter(this);

                return modernAdapter.GetPreferredSizeCore(proposedConstraints) + Padding.Size;
            }

            ButtonStandardAdapter adapter = new(this);
            return adapter.GetPreferredSizeCore(proposedConstraints);
        }

        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 check-box or text
        size.Height = Math.Max(size.Height + 5, _flatSystemStyleMinimumHeight);
        return size + Padding.Size;
    }

    internal override Rectangle OverChangeRectangle
    {
        get
        {
            if (IsToggleSwitchAppearance
                || Appearance == Appearance.Button
                || !OwnerDraw)
            {
                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
            {
                // Popup mouseover rectangle is actually bigger than GetCheckmarkRectangle
                return Adapter.CommonLayout().Layout().CheckBounds;
            }
        }
    }

    internal override Rectangle DownChangeRectangle
    {
        get
        {
            if (IsToggleSwitchAppearance
                || Appearance == Appearance.Button
                || !OwnerDraw)
            {
                return base.DownChangeRectangle;
            }
            else
            {
                // Popup mouseover rectangle is actually bigger than GetCheckmarkRectangle()
                return Adapter.CommonLayout().Layout().CheckBounds;
            }
        }
    }

    internal override bool SupportsUiaProviders => true;

    /// <summary>
    ///  Gets or sets a value indicating the alignment of the text on the checkbox control.
    /// </summary>
    [Localizable(true)]
    [DefaultValue(ContentAlignment.MiddleLeft)]
    public override ContentAlignment TextAlign
    {
        get => base.TextAlign;
        set => base.TextAlign = value;
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the check box will allow three check states rather than two.
    /// </summary>
    [DefaultValue(false)]
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.CheckBoxThreeStateDescr))]
    public bool ThreeState
    {
        get => _threeState;
        set
        {
            if (_threeState == value)
            {
                return;
            }

            _toggleSwitchRenderer?.StopAnimation();
            _threeState = value;
            ResetAdapter();
            UpdateOwnerDraw();
            UpdateToggleSwitchStyles();
            LayoutTransaction.DoLayoutIf(AutoSize, ParentInternal, this, nameof(ThreeState));

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

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

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

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

    protected virtual void OnAppearanceChanged(EventArgs e)
    {
        if (Events[s_appearanceChangedEvent] is EventHandler eh)
        {
            eh(this, e);
        }
    }

    /// <summary>
    ///  Raises the <see cref="CheckedChanged"/>  event.
    /// </summary>
    protected virtual void OnCheckedChanged(EventArgs e)
    {
        NotifyAccessibilityStateChanged();

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

    /// <summary>
    ///  Raises the <see cref="CheckStateChanged"/> event.
    /// </summary>
    protected virtual void OnCheckStateChanged(EventArgs e)
    {
        if (OwnerDraw)
        {
            Refresh();
        }

        if (_notifyAccessibilityStateChangedNeeded)
        {
            NotifyAccessibilityStateChanged();
        }

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

    private void NotifyAccessibilityStateChanged()
    {
        if (FlatStyle == FlatStyle.System)
        {
            AccessibilityNotifyClients(AccessibleEvents.SystemCaptureStart, -1);
        }

        // 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);
        }

        if (FlatStyle == FlatStyle.System)
        {
            AccessibilityNotifyClients(AccessibleEvents.SystemCaptureEnd, -1);
        }
    }

    protected override void OnClick(EventArgs e)
    {
        if (AutoCheck)
        {
            switch (CheckState)
            {
                case CheckState.Unchecked:
                    CheckState = CheckState.Checked;
                    break;
                case CheckState.Checked:
                    if (ThreeState)
                    {
                        CheckState = CheckState.Indeterminate;

                        // If the check box is clicked as a result of AccObj::DoDefaultAction then the native check box
                        // does not fire OBJ_STATE_CHANGE event when going to Indeterminate state and we need to.
                        if (AccObjDoDefaultAction)
                        {
                            AccessibilityNotifyClients(AccessibleEvents.StateChange, -1);
                        }
                    }
                    else
                    {
                        CheckState = CheckState.Unchecked;
                    }

                    break;
                default:
                    CheckState = CheckState.Unchecked;
                    break;
            }
        }

        base.OnClick(e);
    }

    /// <summary>
    ///  We override this to ensure that the control's click values are set up correctly.
    /// </summary>
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);

        if (IsHandleCreated)
        {
            PInvokeCore.SendMessage(this, PInvoke.BM_SETCHECK, (WPARAM)(int)_checkState);
        }

        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);

        // Entering or leaving the modern toggle-switch appearance changes whether the control is owner-drawn.
        UpdateOwnerDraw();
        UpdateToggleSwitchStyles();

        if (IsHandleCreated)
        {
            Invalidate();
        }
    }

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

    internal ContentAlignment RtlTranslatedCheckAlign
        => RtlTranslateContent(CheckAlign);

    internal bool ShowFocusCuesInternal => ShowFocusCues;

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

        base.Dispose(disposing);
    }

    /// <summary>
    ///  Raises the <see cref="ButtonBase.OnMouseUp"/> event.
    /// </summary>
    protected override void OnMouseUp(MouseEventArgs mevent)
    {
        // It's best not to have the mouse captured while running Click events
        if (mevent.Button == MouseButtons.Left
            && MouseIsPressed
            && MouseIsDown
            && PInvoke.WindowFromPoint(PointToScreen(mevent.Location)) == HWND)
        {
            // Paint in raised state.
            ResetFlagsandPaint();
            if (!ValidationCancelled)
            {
                if (Capture)
                {
                    OnClick(mevent);
                }

                OnMouseClick(mevent);
            }
        }

        base.OnMouseUp(mevent);
    }

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

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

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

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

    /// <summary>
    ///  Overridden to handle mnemonics properly.
    /// </summary>
    protected internal override bool ProcessMnemonic(char charCode)
    {
        if (UseMnemonic && IsMnemonic(charCode, Text) && CanSelect)
        {
            if (Focus())
            {
                // Paint in raised state...
                ResetFlagsandPaint();
                if (!ValidationCancelled)
                {
                    OnClick(EventArgs.Empty);
                }
            }

            return true;
        }

        return false;
    }

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