// 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.VisualStyles;
using static System.Windows.Forms.DarkModeButtonColors;

namespace System.Windows.Forms;

/// <summary>
///  Provides methods for rendering a button with System FlatStyle in dark mode.
/// </summary>
internal class SystemButtonDarkModeRenderer : ButtonDarkModeRendererBase
{
    // UI constants
    private const int CornerRadius = 8;
    private const int FocusIndicatorCornerRadius = 6;

    private const int DefaultButtonBorderThickness = 0;
    private const int NonDefaultButtonBorderThickness = 0;
    private const int FocusedButtonBorderThickness = 3;
    private const int DarkBorderGapThickness = 2;
    private const int SystemStylePadding = FocusedButtonBorderThickness + DarkBorderGapThickness;

    private const int DefaultBackgroundColorOffset = 20;

    private protected override Padding PaddingCore { get; } = new Padding(SystemStylePadding);

    private protected override bool PaintParentBackground => true;

    /// <summary>
    ///  Draws button background with system styling (larger rounded corners).
    /// </summary>
    public override Rectangle DrawButtonBackground(
        Graphics graphics,
        Rectangle bounds,
        PushButtonState state,
        bool isDefault,
        bool focused,
        Color backColor)
    {
        GraphicsState? saved = graphics.Save();
        try
        {
            graphics.SmoothingMode = SmoothingMode.AntiAlias;

            RectangleF pathBounds = GetPathBounds(bounds);
            using GraphicsPath fillPath = CreateRoundedPath(pathBounds, FocusIndicatorCornerRadius);
            using var brush = backColor.GetCachedSolidBrushScope();
            graphics.FillPath(brush, fillPath);
        }
        finally
        {
            if (saved is not null)
            {
                graphics.Restore(saved);
            }
        }

        // Return content bounds (area inside the button for text/image)
        return bounds;
    }

    /// <summary>
    ///  Draws a focus indicator using a white thicker border.
    /// </summary>
    public override void DrawFocusIndicator(Graphics graphics, Rectangle contentBounds, bool isDefault)
    {
        GraphicsState? saved = graphics.Save();
        try
        {
            graphics.SmoothingMode = SmoothingMode.AntiAlias;

            RectangleF outerBounds = GetPathBounds(contentBounds);
            float outerRadius = FocusIndicatorCornerRadius + (2 * SystemStylePadding);
            Color focusColor = ResolveBorderColor(Color.White);
            using var focusBrush = focusColor.GetCachedSolidBrushScope();
            using GraphicsPath focusPath = CreateRingPath(
                outerBounds,
                outerRadius,
                FocusedButtonBorderThickness);
            graphics.FillPath(focusBrush, focusPath);
        }
        finally
        {
            if (saved is not null)
            {
                graphics.Restore(saved);
            }
        }
    }

    /// <summary>
    ///  Gets the text color appropriate for the button state and type.
    /// </summary>
    public override Color GetTextColor(PushButtonState state, bool isDefault, Color backColor) =>
        state == PushButtonState.Disabled
            ? DefaultColors.DisabledTextColor
            : isDefault
                ? DefaultColors.StandardBackColor
                : DefaultColors.AcceptButtonTextColor;

    /// <summary>
    ///  Gets the background color appropriate for the button state and type.
    /// </summary>
    public override Color GetBackgroundColor(PushButtonState state, bool isDefault) =>
        // For default button in System style, use a darker version of the background color
        isDefault
            ? state switch
            {
                PushButtonState.Normal => Color.FromArgb(
                    DefaultColors.StandardBackColor.R - DefaultBackgroundColorOffset,
                    DefaultColors.StandardBackColor.G - DefaultBackgroundColorOffset,
                    DefaultColors.StandardBackColor.B - DefaultBackgroundColorOffset),
                PushButtonState.Hot => Color.FromArgb(
                    DefaultColors.HoverBackColor.R - DefaultBackgroundColorOffset,
                    DefaultColors.HoverBackColor.G - DefaultBackgroundColorOffset,
                    DefaultColors.HoverBackColor.B - DefaultBackgroundColorOffset),
                PushButtonState.Pressed => Color.FromArgb(
                    DefaultColors.PressedBackColor.R - DefaultBackgroundColorOffset,
                    DefaultColors.PressedBackColor.G - DefaultBackgroundColorOffset,
                    DefaultColors.PressedBackColor.B - DefaultBackgroundColorOffset),
                PushButtonState.Disabled => DefaultColors.DisabledBackColor,
                _ => DefaultColors.StandardBackColor
            }
            : state switch
            {
                PushButtonState.Normal => DefaultColors.StandardBackColor,
                PushButtonState.Hot => DefaultColors.HoverBackColor,
                PushButtonState.Pressed => DefaultColors.PressedBackColor,
                PushButtonState.Disabled => DefaultColors.DisabledBackColor,
                _ => DefaultColors.StandardBackColor
            };

    /// <summary>
    ///  Draws the button border based on the current state, using anti-aliasing and an additional inner border.
    /// </summary>
    public void DrawButtonBorder(
        Graphics graphics,
        Rectangle bounds,
        PushButtonState state,
        bool isDefault,
        bool isFocused)
    {
        // Skip border drawing for disabled state
        if (state == PushButtonState.Disabled)
        {
            return;
        }

        // Don't draw regular border if focus border is already drawn
        if (isFocused)
        {
            return;
        }

        // Outer border path
        Rectangle borderRect = Rectangle.Inflate(bounds, -SystemStylePadding, -SystemStylePadding);

        using GraphicsPath borderPath = CreateRoundedPath(
            GetPathBounds(borderRect),
            CornerRadius);

        // We need to implement a subtle 3d effect around the already
        // painted filling. We do this by drawing a border with a 1px pen,
        // which is - with brighter colors - top and right a bit darker than
        // the fill color, and bottom and left yet another bit darker.
        //
        // For darker fill colors, we use a slightly lighter color for the top and right sides,
        // and a yet bit lighter color for the bottom and left sides.
        // We never change the color for the borders, we just adjust the brightness.

        // Get base color for the border based on button state and type
        Color backColor = GetBackgroundColor(state, isDefault);

        // For normal colors, make borders darker
        // For dark colors, make borders lighter
        bool isDarkColor = backColor.GetBrightness() < 0.5;

        // Top-left border (slightly lighter/darker)
        Color topLeftColor = isDarkColor
            ? ControlPaint.Light(backColor, 0.2f)
            : ControlPaint.Dark(backColor, 0.1f);

        // Bottom-right border (more pronounced light/dark)
        Color bottomRightColor = isDarkColor
            ? ControlPaint.Light(backColor, 0.4f)
            : ControlPaint.Dark(backColor, 0.2f);

        // Determine border thickness
        int borderThickness = isDefault ? DefaultButtonBorderThickness : NonDefaultButtonBorderThickness;

        // Save graphics state to restore anti-aliasing settings later
        GraphicsState? graphicState = null;

        try
        {
            graphicState = graphics.Save();
            graphics.SmoothingMode = SmoothingMode.AntiAlias;

            // Draw top-left border segment
            using var topLeftPen = topLeftColor.GetCachedPenScope(borderThickness);
            graphics.DrawPath(topLeftPen, GetTopLeftSegmentPath(borderRect, CornerRadius));

            // Draw bottom-right border segment
            using var bottomRightPen = bottomRightColor.GetCachedPenScope(borderThickness);
            graphics.DrawPath(bottomRightPen, GetBottomRightSegmentPath(borderRect, CornerRadius));
        }
        finally
        {
            // Restore graphics state
            if (graphicState is not null)
            {
                graphics.Restore(graphicState);
            }
        }
    }

    /// <summary>
    ///  Creates a path for the top and left segments of a rounded rectangle.
    /// </summary>
    private static GraphicsPath GetTopLeftSegmentPath(Rectangle bounds, int radius)
    {
        GraphicsPath path = new();

        int diameter = radius * 2;

        // Top left corner arc
        Rectangle arcRect = new(bounds.Location, new Size(diameter, diameter));
        path.AddArc(arcRect, 180, 90);

        // Top line
        path.AddLine(bounds.Left + radius, bounds.Top, bounds.Right - radius, bounds.Top);

        // Top right corner arc (just the top portion)
        arcRect.X = bounds.Right - diameter;
        path.AddArc(arcRect, 270, 45);

        // Path back to middle of right side
        path.AddLine(
            bounds.Right - (int)(radius * Math.Sin(Math.PI / 4)),
            bounds.Top + (int)(radius * (1 - Math.Cos(Math.PI / 4))),
            bounds.Right,
            bounds.Top + bounds.Height / 2);

        // Path to middle of bottom
        path.AddLine(bounds.Right, bounds.Top + bounds.Height / 2, bounds.Left + bounds.Width / 2, bounds.Bottom);

        // Path to bottom left corner
        path.AddLine(bounds.Left + bounds.Width / 2, bounds.Bottom, bounds.Left, bounds.Bottom - bounds.Height / 2);

        // Path back to start
        path.AddLine(bounds.Left, bounds.Bottom - bounds.Height / 2, bounds.Left, bounds.Top + radius);

        return path;
    }

    /// <summary>
    ///  Creates a path for the bottom and right segments of a rounded rectangle.
    /// </summary>
    private static GraphicsPath GetBottomRightSegmentPath(Rectangle bounds, int radius)
    {
        GraphicsPath path = new();

        int diameter = radius * 2;

        // Start from middle of top edge
        path.AddLine(bounds.Left + bounds.Width / 2, bounds.Top, bounds.Right, bounds.Top + bounds.Height / 2);

        // Right line
        path.AddLine(bounds.Right, bounds.Top + bounds.Height / 2, bounds.Right, bounds.Bottom - radius);

        // Bottom right corner arc
        Rectangle arcRect = new(bounds.Right - diameter, bounds.Bottom - diameter, diameter, diameter);
        path.AddArc(arcRect, 0, 90);

        // Bottom line
        path.AddLine(bounds.Right - radius, bounds.Bottom, bounds.Left + radius, bounds.Bottom);

        // Bottom left corner arc
        arcRect.X = bounds.Left;
        path.AddArc(arcRect, 90, 45);

        // Path back to middle of left side
        path.AddLine(
            bounds.Left + (int)(radius * (1 - Math.Cos(Math.PI / 4))),
            bounds.Bottom - (int)(radius * Math.Sin(Math.PI / 4)),
            bounds.Left,
            bounds.Top + bounds.Height / 2);

        // Close the path back to start
        path.AddLine(bounds.Left, bounds.Top + bounds.Height / 2, bounds.Left + bounds.Width / 2, bounds.Top);

        return path;
    }

    private static RectangleF GetPathBounds(Rectangle bounds)
        => new(
            bounds.X,
            bounds.Y,
            Math.Max(1, bounds.Width - 1),
            Math.Max(1, bounds.Height - 1));

    private static RectangleF Inset(RectangleF bounds, float inset)
        => new(
            bounds.X + inset,
            bounds.Y + inset,
            Math.Max(1, bounds.Width - (2 * inset)),
            Math.Max(1, bounds.Height - (2 * inset)));

    private static GraphicsPath CreateRoundedPath(RectangleF bounds, float radius)
    {
        GraphicsPath path = new();
        float clampedRadius = Math.Clamp(radius, 1, Math.Min(bounds.Width, bounds.Height));
        path.AddRoundedRectangle(bounds, new SizeF(clampedRadius, clampedRadius));
        return path;
    }

    private static GraphicsPath CreateRingPath(
        RectangleF outerBounds,
        float outerRadius,
        float thickness)
    {
        GraphicsPath path = CreateRoundedPath(outerBounds, outerRadius);
        RectangleF innerBounds = Inset(outerBounds, thickness);
        float innerRadius = Math.Max(1, outerRadius - (2 * thickness));
        using GraphicsPath innerPath = CreateRoundedPath(innerBounds, innerRadius);
        path.FillMode = FillMode.Alternate;
        path.AddPath(innerPath, connect: false);
        return path;
    }
}
