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

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms.Rendering.Animation;
using System.Windows.Forms.Rendering.Button;

namespace System.Windows.Forms.Rendering.RadioButton;

/// <summary>
///  Animates and draws the modern normal-appearance RadioButton glyph.
/// </summary>
internal sealed class AnimatedRadioGlyphRenderer : AnimatedControlRenderer
{
    private const int AnimationDuration = 200;
    private const float HoverGrowth = 1.1f;

    private float _dotScaleCurrent;
    private float _dotScaleStart;
    private float _dotScaleTarget;
    private float _focusCurrent;
    private float _focusStart;
    private float _focusTarget;
    private float _hoverCurrent;
    private float _hoverStart;
    private float _hoverTarget;
    private bool _interactionInitialized;
    private bool _stateInitialized;

    public AnimatedRadioGlyphRenderer(Control control) : base(control)
    {
    }

    internal void NotifyCheckedChanged(bool newChecked)
    {
        float dotScaleTarget = newChecked ? 1f : 0f;
        if (!_stateInitialized)
        {
            _stateInitialized = true;
            _dotScaleCurrent = _dotScaleStart = _dotScaleTarget = dotScaleTarget;
            return;
        }

        if (_dotScaleTarget == dotScaleTarget)
        {
            return;
        }

        _dotScaleTarget = dotScaleTarget;
        RestartAnimation();
    }

    internal void SetInteractionState(bool hovered, bool focused)
    {
        float hoverTarget = hovered ? 1f : 0f;
        float focusTarget = focused ? 1f : 0f;
        if (!_interactionInitialized)
        {
            _interactionInitialized = true;
            _hoverCurrent = _hoverStart = _hoverTarget = hoverTarget;
            _focusCurrent = _focusStart = _focusTarget = focusTarget;
            return;
        }

        if (_hoverTarget == hoverTarget && _focusTarget == focusTarget)
        {
            return;
        }

        _hoverTarget = hoverTarget;
        _focusTarget = focusTarget;
        RestartAnimation();
    }

    internal void DrawGlyph(
        Graphics graphics,
        Rectangle bounds,
        FlatStyle flatStyle,
        bool enabled,
        bool hovered,
        bool focused,
        Color? customOnColor,
        Color? customBorderColor)
    {
        SetInteractionState(hovered, focused);

        bool isDark = Application.IsDarkModeEnabled;
        bool highContrast = SystemInformation.HighContrast;
        Color onColor = highContrast
            ? SystemColors.Highlight
            : customOnColor ?? WindowsAccentColor;

        Color borderColor = highContrast
            ? SystemColors.WindowText
            : customBorderColor
                ?? (isDark
                    ? Color.FromArgb(0x9B, 0x9B, 0x9B)
                    : SystemColors.ControlDark);

        Color backColor = highContrast
            ? SystemColors.Window
            : isDark
                ? Color.FromArgb(0x2D, 0x2D, 0x2D)
                : Color.White;

        if (!enabled)
        {
            onColor = highContrast
                ? SystemColors.GrayText
                : isDark
                    ? Color.FromArgb(0x55, 0x55, 0x55)
                    : Color.FromArgb(0xC0, 0xC0, 0xC0);
            borderColor = highContrast
                ? SystemColors.GrayText
                : isDark
                    ? Color.FromArgb(0x45, 0x45, 0x45)
                    : Color.FromArgb(0xD0, 0xD0, 0xD0);
        }

        float focus = enabled && !highContrast ? _focusCurrent : 0f;
        onColor = ApplyInteractionShade(onColor, focus);
        borderColor = ApplyInteractionShade(borderColor, focus);
        backColor = ApplyInteractionShade(backColor, focus);

        // Blend the outer circle toward the accent based on animated checked progress
        // (_dotScaleCurrent), so check and uncheck transitions stay smooth and symmetric.
        float checkedProgress = Math.Clamp(
            _dotScaleCurrent,
            0f,
            1f);
        Color activeBackColor = LerpColor(backColor, onColor, checkedProgress);
        Color activeBorderColor = LerpColor(borderColor, onColor, checkedProgress);

        float normalOuterScale = 1f / HoverGrowth;
        float outerScale = Lerp(normalOuterScale, 1f, enabled ? _hoverCurrent : 0f);
        RectangleF outerBounds = ScaleFromCenter(bounds, outerScale);
        RectangleF normalBounds = ScaleFromCenter(bounds, normalOuterScale);

        GraphicsState? saved = graphics.Save();
        try
        {
            graphics.SmoothingMode = SmoothingMode.AntiAlias;

            using (var brush = activeBackColor.GetCachedSolidBrushScope())
            {
                graphics.FillEllipse(brush, outerBounds);
            }

            int borderThickness = Math.Max(
                1,
                Control.LogicalToDeviceUnits(flatStyle == FlatStyle.Popup ? 2 : 1));

            using (var pen = new Pen(activeBorderColor, borderThickness))
            {
                graphics.DrawEllipse(pen, outerBounds);
            }

            if (_dotScaleCurrent > 0.001f)
            {
                float dotDiameter = normalBounds.Width * 0.5f * _dotScaleCurrent;
                RectangleF dotRectangle = new(
                    normalBounds.X + ((normalBounds.Width - dotDiameter) / 2f),
                    normalBounds.Y + ((normalBounds.Height - dotDiameter) / 2f),
                    dotDiameter,
                    dotDiameter);

                Color dotColor = highContrast
                    ? SystemColors.HighlightText
                    : PopupButtonColorMath.GetReadableForeColor(onColor);
                using var dotBrush = dotColor.GetCachedSolidBrushScope();
                graphics.FillEllipse(dotBrush, dotRectangle);
            }
        }
        finally
        {
            if (saved is not null)
            {
                graphics.Restore(saved);
            }
        }
    }

    public override void AnimationProc(float animationProgress)
    {
        base.AnimationProc(animationProgress);
        float easedProgress = EaseOut(animationProgress);
        _dotScaleCurrent = Lerp(_dotScaleStart, _dotScaleTarget, easedProgress);
        _focusCurrent = Lerp(_focusStart, _focusTarget, easedProgress);
        _hoverCurrent = Lerp(_hoverStart, _hoverTarget, easedProgress);
        Invalidate();
    }

    public override void RenderControl(Graphics graphics)
    {
    }

    protected override (int animationDuration, AnimationCycle animationCycle) OnAnimationStarted()
    {
        _dotScaleStart = _dotScaleCurrent;
        _focusStart = _focusCurrent;
        _hoverStart = _hoverCurrent;
        AnimationProgress = 0;
        return (AnimationDuration, AnimationCycle.Once);
    }

    protected override void OnAnimationStopped()
    {
    }

    protected override void OnAnimationEnded()
    {
        StopAnimation();
        _dotScaleCurrent = _dotScaleTarget;
        _focusCurrent = _focusTarget;
        _hoverCurrent = _hoverTarget;
        AnimationProgress = 1;
        Invalidate();
    }

    private static float EaseOut(float progress)
        => 1 - ((1 - progress) * (1 - progress));

    private static float Lerp(float from, float to, float progress)
        => from + ((to - from) * Math.Clamp(progress, 0f, 1f));

    private static Color LerpColor(Color from, Color to, float progress)
    {
        progress = Math.Clamp(progress, 0f, 1f);

        return Color.FromArgb(
            LerpChannel(from.A, to.A, progress),
            LerpChannel(from.R, to.R, progress),
            LerpChannel(from.G, to.G, progress),
            LerpChannel(from.B, to.B, progress));

        static int LerpChannel(int from, int to, float progress)
            => from + (int)((to - from) * progress);
    }

    private static RectangleF ScaleFromCenter(Rectangle bounds, float scale)
    {
        float width = bounds.Width * scale;
        float height = bounds.Height * scale;

        return new(
            bounds.X + ((bounds.Width - width) / 2f),
            bounds.Y + ((bounds.Height - height) / 2f),
            width,
            height);
    }
}
