using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace PCL;

public class MyDropShadow : Decorator
{
    public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Color),
        typeof(MyDropShadow),
        new FrameworkPropertyMetadata(Color.FromArgb(0x71, 0x0, 0x0, 0x0),
            FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes));

    public static readonly DependencyProperty ShadowRadiusProperty = DependencyProperty.Register("ShadowRadius",
        typeof(double), typeof(MyDropShadow),
        new FrameworkPropertyMetadata(5d, FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes));

    public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius",
        typeof(CornerRadius), typeof(MyDropShadow),
        new FrameworkPropertyMetadata(new CornerRadius(), FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes),
        IsCornerRadiusValid);

    private static Brush[] _commonBrushes;
    private static CornerRadius _commonCornerRadius;
    private static readonly object _resourceAccess = new();
    private Brush[] _brushes;

    /// <summary>
    ///     阴影颜色。
    /// </summary>
    public Color Color
    {
        get => (Color)GetValue(ColorProperty);
        set => SetValue(ColorProperty, value);
    }

    /// <summary>
    ///     阴影模糊半径。
    /// </summary>
    public double ShadowRadius
    {
        get => (double)GetValue(ShadowRadiusProperty);
        set => SetValue(ShadowRadiusProperty, value);
    }

    /// <summary>
    ///     圆角大小。
    /// </summary>
    public CornerRadius CornerRadius
    {
        get => (CornerRadius)GetValue(CornerRadiusProperty);
        set => SetValue(CornerRadiusProperty, value);
    }

    private static bool IsCornerRadiusValid(object value)
    {
        var cr = (CornerRadius)value;
        return !(cr.TopLeft < 0.0d || cr.TopRight < 0.0d || cr.BottomLeft < 0.0d || cr.BottomRight < 0.0d ||
                 double.IsNaN(cr.TopLeft) || double.IsNaN(cr.TopRight) || double.IsNaN(cr.BottomLeft) ||
                 double.IsNaN(cr.BottomRight) || double.IsInfinity(cr.TopLeft) || double.IsInfinity(cr.TopRight) ||
                 double.IsInfinity(cr.BottomLeft) || double.IsInfinity(cr.BottomRight));
    }


    // =======================================
    // 渲染
    // =======================================


    protected override void OnRender(DrawingContext drawingContext)
    {
        var cornerRadius = CornerRadius;
        var shadowBounds = new Rect(0d, 0d, RenderSize.Width, RenderSize.Height);
        var color = Color;

        if (shadowBounds.Width > 0d && shadowBounds.Height > 0d && color.A > 0)
        {
            var centerWidth = shadowBounds.Right - shadowBounds.Left - 2d * ShadowRadius;
            var centerHeight = shadowBounds.Bottom - shadowBounds.Top - 2d * ShadowRadius;
            var maxRadius = Math.Min(centerWidth * 0.5d, centerHeight * 0.5d);
            cornerRadius.TopLeft = Math.Min(cornerRadius.TopLeft, maxRadius);
            cornerRadius.TopRight = Math.Min(cornerRadius.TopRight, maxRadius);
            cornerRadius.BottomLeft = Math.Min(cornerRadius.BottomLeft, maxRadius);
            cornerRadius.BottomRight = Math.Min(cornerRadius.BottomRight, maxRadius);
            var brushes = GetBrushes(color, cornerRadius);
            var centerTop = shadowBounds.Top + ShadowRadius;
            var centerLeft = shadowBounds.Left + ShadowRadius;
            var centerRight = shadowBounds.Right - ShadowRadius;
            var centerBottom = shadowBounds.Bottom - ShadowRadius;
            var guidelineSetX = new[]
            {
                centerLeft, centerLeft + cornerRadius.TopLeft, centerRight - cornerRadius.TopRight,
                centerLeft + cornerRadius.BottomLeft, centerRight - cornerRadius.BottomRight, centerRight
            };
            var guidelineSetY = new[]
            {
                centerTop, centerTop + cornerRadius.TopLeft, centerTop + cornerRadius.TopRight,
                centerBottom - cornerRadius.BottomLeft, centerBottom - cornerRadius.BottomRight, centerBottom
            };
            drawingContext.PushGuidelineSet(new GuidelineSet(guidelineSetX, guidelineSetY));
            cornerRadius.TopLeft += ShadowRadius;
            cornerRadius.TopRight += ShadowRadius;
            cornerRadius.BottomLeft += ShadowRadius;
            cornerRadius.BottomRight += ShadowRadius;
            var topLeft = new Rect(shadowBounds.Left, shadowBounds.Top, cornerRadius.TopLeft, cornerRadius.TopLeft);
            drawingContext.DrawRectangle(brushes[(int)Placement.TopLeft], null, topLeft);
            var topWidth = guidelineSetX[2] - guidelineSetX[1];

            if (topWidth > 0d)
            {
                var top = new Rect(guidelineSetX[1], shadowBounds.Top, topWidth, ShadowRadius);
                drawingContext.DrawRectangle(brushes[(int)Placement.Top], null, top);
            }

            var topRight = new Rect(guidelineSetX[2], shadowBounds.Top, cornerRadius.TopRight, cornerRadius.TopRight);
            drawingContext.DrawRectangle(brushes[(int)Placement.TopRight], null, topRight);
            var leftHeight = guidelineSetY[3] - guidelineSetY[1];

            if (leftHeight > 0d)
            {
                var left = new Rect(shadowBounds.Left, guidelineSetY[1], ShadowRadius, leftHeight);
                drawingContext.DrawRectangle(brushes[(int)Placement.Left], null, left);
            }

            var rightHeight = guidelineSetY[4] - guidelineSetY[2];

            if (rightHeight > 0d)
            {
                var right = new Rect(guidelineSetX[5], guidelineSetY[2], ShadowRadius, rightHeight);
                drawingContext.DrawRectangle(brushes[(int)Placement.Right], null, right);
            }

            var bottomLeft = new Rect(shadowBounds.Left, guidelineSetY[3], cornerRadius.BottomLeft,
                cornerRadius.BottomLeft);
            drawingContext.DrawRectangle(brushes[(int)Placement.BottomLeft], null, bottomLeft);
            var bottomWidth = guidelineSetX[4] - guidelineSetX[3];

            if (bottomWidth > 0d)
            {
                var bottom = new Rect(guidelineSetX[3], guidelineSetY[5], bottomWidth, ShadowRadius);
                drawingContext.DrawRectangle(brushes[(int)Placement.Bottom], null, bottom);
            }

            var bottomRight = new Rect(guidelineSetX[4], guidelineSetY[4], cornerRadius.BottomRight,
                cornerRadius.BottomRight);
            drawingContext.DrawRectangle(brushes[(int)Placement.BottomRight], null, bottomRight);

            if (cornerRadius.TopLeft == ShadowRadius && cornerRadius.TopLeft == cornerRadius.TopRight &&
                cornerRadius.TopLeft == cornerRadius.BottomLeft && cornerRadius.TopLeft == cornerRadius.BottomRight)
            {
                var center = new Rect(guidelineSetX[0], guidelineSetY[0], centerWidth, centerHeight);
                drawingContext.DrawRectangle(brushes[(int)Placement.Center], null, center);
            }
            else
            {
                var figure = new PathFigure();

                if (cornerRadius.TopLeft > ShadowRadius)
                {
                    figure.StartPoint = new Point(guidelineSetX[1], guidelineSetY[0]);
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[1], guidelineSetY[1]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[1]), true));
                }
                else
                {
                    figure.StartPoint = new Point(guidelineSetX[0], guidelineSetY[0]);
                }

                if (cornerRadius.BottomLeft > ShadowRadius)
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[3]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[3]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[5]), true));
                }
                else
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[5]), true));
                }

                if (cornerRadius.BottomRight > ShadowRadius)
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[5]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[4]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[4]), true));
                }
                else
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[5]), true));
                }

                if (cornerRadius.TopRight > ShadowRadius)
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[2]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[2]), true));
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[0]), true));
                }
                else
                {
                    figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[0]), true));
                }

                figure.IsClosed = true;
                figure.Freeze();
                var geometry = new PathGeometry();
                geometry.Figures.Add(figure);
                geometry.Freeze();
                drawingContext.DrawGeometry(brushes[(int)Placement.Center], null, geometry);
            }

            drawingContext.Pop();
        }
    }

    private static void ClearBrushes(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        ((MyDropShadow)o)._brushes = null;
    }

    private GradientStopCollection CreateStops(Color c, double cornerRadius)
    {
        var gradientScale = 1d / (ShadowRadius + cornerRadius);
        var gsc = new GradientStopCollection();
        var stopColor = c;
        gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.1d + cornerRadius) * gradientScale));
        stopColor.A = (byte)Math.Round(0.74336d * c.A);
        gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.3d + cornerRadius) * gradientScale));
        stopColor.A = (byte)Math.Round(0.38053d * c.A);
        gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.5d + cornerRadius) * gradientScale));
        stopColor.A = (byte)Math.Round(0.12389d * c.A);
        gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.7d + cornerRadius) * gradientScale));
        stopColor.A = (byte)Math.Round(0.02654d * c.A);
        gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.9d + cornerRadius) * gradientScale));
        stopColor.A = 0;
        gsc.Add(new GradientStop(stopColor, (ShadowRadius + cornerRadius) * gradientScale));
        gsc.Freeze();
        return gsc;
    }

    private Brush[] CreateBrushes(Color c, CornerRadius cornerRadius)
    {
        var brushes = new Brush[9];
        brushes[(int)Placement.Center] = new SolidColorBrush(c);
        brushes[(int)Placement.Center].Freeze();
        var sideStops = CreateStops(c, 0d);
        var top = new LinearGradientBrush(sideStops, new Point(0d, 1d), new Point(0d, 0d));
        top.Freeze();
        brushes[(int)Placement.Top] = top;
        var left = new LinearGradientBrush(sideStops, new Point(1d, 0d), new Point(0d, 0d));
        left.Freeze();
        brushes[(int)Placement.Left] = left;
        var right = new LinearGradientBrush(sideStops, new Point(0d, 0d), new Point(1d, 0d));
        right.Freeze();
        brushes[(int)Placement.Right] = right;
        var bottom = new LinearGradientBrush(sideStops, new Point(0d, 0d), new Point(0d, 1d));
        bottom.Freeze();
        brushes[(int)Placement.Bottom] = bottom;
        GradientStopCollection topLeftStops;

        if (cornerRadius.TopLeft == 0d)
            topLeftStops = sideStops;
        else
            topLeftStops = CreateStops(c, cornerRadius.TopLeft);

        var topLeft = new RadialGradientBrush(topLeftStops)
        {
            RadiusX = 1d,
            RadiusY = 1d,
            Center = new Point(1d, 1d),
            GradientOrigin = new Point(1d, 1d)
        };
        topLeft.Freeze();
        brushes[(int)Placement.TopLeft] = topLeft;
        GradientStopCollection topRightStops;

        if (cornerRadius.TopRight == 0d)
            topRightStops = sideStops;
        else if (cornerRadius.TopRight == cornerRadius.TopLeft)
            topRightStops = topLeftStops;
        else
            topRightStops = CreateStops(c, cornerRadius.TopRight);

        var topRight = new RadialGradientBrush(topRightStops)
        {
            RadiusX = 1d,
            RadiusY = 1d,
            Center = new Point(0d, 1d),
            GradientOrigin = new Point(0d, 1d)
        };
        topRight.Freeze();
        brushes[(int)Placement.TopRight] = topRight;
        GradientStopCollection bottomLeftStops;

        if (cornerRadius.BottomLeft == 0d)
            bottomLeftStops = sideStops;
        else if (cornerRadius.BottomLeft == cornerRadius.TopLeft)
            bottomLeftStops = topLeftStops;
        else if (cornerRadius.BottomLeft == cornerRadius.TopRight)
            bottomLeftStops = topRightStops;
        else
            bottomLeftStops = CreateStops(c, cornerRadius.BottomLeft);

        var bottomLeft = new RadialGradientBrush(bottomLeftStops)
        {
            RadiusX = 1d,
            RadiusY = 1d,
            Center = new Point(1d, 0d),
            GradientOrigin = new Point(1d, 0d)
        };
        bottomLeft.Freeze();
        brushes[(int)Placement.BottomLeft] = bottomLeft;
        GradientStopCollection bottomRightStops;

        if (cornerRadius.BottomRight == 0d)
            bottomRightStops = sideStops;
        else if (cornerRadius.BottomRight == cornerRadius.TopLeft)
            bottomRightStops = topLeftStops;
        else if (cornerRadius.BottomRight == cornerRadius.TopRight)
            bottomRightStops = topRightStops;
        else if (cornerRadius.BottomRight == cornerRadius.BottomLeft)
            bottomRightStops = bottomLeftStops;
        else
            bottomRightStops = CreateStops(c, cornerRadius.BottomRight);

        var bottomRight = new RadialGradientBrush(bottomRightStops)
        {
            RadiusX = 1d,
            RadiusY = 1d,
            Center = new Point(0d, 0d),
            GradientOrigin = new Point(0d, 0d)
        };
        bottomRight.Freeze();
        brushes[(int)Placement.BottomRight] = bottomRight;
        return brushes;
    }

    private Brush[] GetBrushes(Color c, CornerRadius cornerRadius)
    {
        if (_commonBrushes is null)
            lock (_resourceAccess)
            {
                if (_commonBrushes is null)
                {
                    _commonBrushes = CreateBrushes(c, cornerRadius);
                    _commonCornerRadius = cornerRadius;
                }
            }

        if (c == ((SolidColorBrush)_commonBrushes[(int)Placement.Center]).Color && cornerRadius == _commonCornerRadius)
        {
            _brushes = null;
            return _commonBrushes;
        }

        if (_brushes is null) _brushes = CreateBrushes(c, cornerRadius);

        return _brushes;
    }

    private enum Placement
    {
        TopLeft = 0,
        Top = 1,
        TopRight = 2,
        Left = 3,
        Center = 4,
        Right = 5,
        BottomLeft = 6,
        Bottom = 7,
        BottomRight = 8
    }
}

// 参考自：https://referencesource.microsoft.com/#PresentationFramework.Aero/parent/Shared/Microsoft/Windows/Themes/SystemDropShadowChrome.cs,6d9c27d92a8128c1