using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using HandyControl.Data;
using HandyControl.Expression.Drawing;
using HandyControl.Tools;
using HandyControl.Tools.Extension;

namespace HandyControl.Controls
{
    public class DashedBorder : Decorator
    {
        private bool _useComplexRenderCodePath;

        private Pen GeometryPenCache { get; set; }

        private Pen LeftPenCache { get; set; }

        private Pen RightPenCache { get; set; }

        private Pen TopPenCache { get; set; }

        private Pen BottomPenCache { get; set; }

        private StreamGeometry BackgroundGeometryCache { get; set; }

        private StreamGeometry BorderGeometryCache { get; set; }

        private static void OnClearPenCache(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var border = (DashedBorder) d;
            border.LeftPenCache = null;
            border.RightPenCache = null;
            border.TopPenCache = null;
            border.BottomPenCache = null;
            border.GeometryPenCache = null;
        }

        public static readonly DependencyProperty BorderThicknessProperty = DependencyProperty.Register(
            "BorderThickness", typeof(Thickness), typeof(DashedBorder), new FrameworkPropertyMetadata(default(Thickness),
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
                OnClearPenCache));

        public Thickness BorderThickness
        {
            get => (Thickness) GetValue(BorderThicknessProperty);
            set => SetValue(BorderThicknessProperty, value);
        }

        public static readonly DependencyProperty BorderDashThicknessProperty = DependencyProperty.Register(
            "BorderDashThickness", typeof(double), typeof(DashedBorder), new FrameworkPropertyMetadata(ValueBoxes.Double0Box,
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
                OnClearPenCache));

        public double BorderDashThickness
        {
            get => (double) GetValue(BorderDashThicknessProperty);
            set => SetValue(BorderDashThicknessProperty, value);
        }

        public static readonly DependencyProperty PaddingProperty = DependencyProperty.Register(
            "Padding", typeof(Thickness), typeof(DashedBorder), new FrameworkPropertyMetadata(default(Thickness),
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

        public Thickness Padding
        {
            get => (Thickness) GetValue(PaddingProperty);
            set => SetValue(PaddingProperty, value);
        }

        public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(
            "CornerRadius", typeof(CornerRadius), typeof(DashedBorder), new FrameworkPropertyMetadata(default(CornerRadius),
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

        public CornerRadius CornerRadius
        {
            get => (CornerRadius) GetValue(CornerRadiusProperty);
            set => SetValue(CornerRadiusProperty, value);
        }

        public static readonly DependencyProperty BorderBrushProperty = DependencyProperty.Register(
            "BorderBrush", typeof(Brush), typeof(DashedBorder), new FrameworkPropertyMetadata(default(Brush),
                FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender,
                OnClearPenCache));

        public Brush BorderBrush
        {
            get => (Brush) GetValue(BorderBrushProperty);
            set => SetValue(BorderBrushProperty, value);
        }

        public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register(
            "Background", typeof(Brush), typeof(DashedBorder), new FrameworkPropertyMetadata(default(Brush),
                FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender,
                OnClearPenCache));

        public Brush Background
        {
            get => (Brush) GetValue(BackgroundProperty);
            set => SetValue(BackgroundProperty, value);
        }

        public static readonly DependencyProperty BorderDashArrayProperty = DependencyProperty.Register(
            "BorderDashArray", typeof(DoubleCollection), typeof(DashedBorder), new FrameworkPropertyMetadata(default(DoubleCollection),
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
                OnClearPenCache));

        public DoubleCollection BorderDashArray
        {
            get => (DoubleCollection) GetValue(BorderDashArrayProperty);
            set => SetValue(BorderDashArrayProperty, value);
        }

        public static readonly DependencyProperty BorderDashCapProperty = DependencyProperty.Register(
            "BorderDashCap", typeof(PenLineCap), typeof(DashedBorder), new FrameworkPropertyMetadata(default(PenLineCap),
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
                OnClearPenCache));

        public PenLineCap BorderDashCap
        {
            get => (PenLineCap) GetValue(BorderDashCapProperty);
            set => SetValue(BorderDashCapProperty, value);
        }

        public static readonly DependencyProperty BorderDashOffsetProperty = DependencyProperty.Register(
            "BorderDashOffset", typeof(double), typeof(DashedBorder), new FrameworkPropertyMetadata(ValueBoxes.Double0Box,
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
                OnClearPenCache));

        public double BorderDashOffset
        {
            get => (double) GetValue(BorderDashOffsetProperty);
            set => SetValue(BorderDashOffsetProperty, value);
        }

        private static Size ConvertThickness2Size(Thickness th) => new Size(th.Left + th.Right, th.Top + th.Bottom);

        private static Rect DeflateRect(Rect rt, Thickness thick) => new Rect(rt.Left + thick.Left,
            rt.Top + thick.Top,
            Math.Max(0.0, rt.Width - thick.Left - thick.Right),
            Math.Max(0.0, rt.Height - thick.Top - thick.Bottom));

        private static bool AreUniformCorners(CornerRadius borderRadii)
        {
            var topLeft = borderRadii.TopLeft;
            return MathHelper.AreClose(topLeft, borderRadii.TopRight) &&
                   MathHelper.AreClose(topLeft, borderRadii.BottomLeft) &&
                   MathHelper.AreClose(topLeft, borderRadii.BottomRight);
        }

        private static void GenerateGeometry(StreamGeometryContext ctx, Rect rect, in Radii radii)
        {
            var topLeft = new Point(radii.LeftTop, 0);
            var topRight = new Point(rect.Width - radii.RightTop, 0);
            var rightTop = new Point(rect.Width, radii.TopRight);
            var rightBottom = new Point(rect.Width, rect.Height - radii.BottomRight);
            var bottomRight = new Point(rect.Width - radii.RightBottom, rect.Height);
            var bottomLeft = new Point(radii.LeftBottom, rect.Height);
            var leftBottom = new Point(0, rect.Height - radii.BottomLeft);
            var leftTop = new Point(0, radii.TopLeft);

            //  top edge
            if (topLeft.X > topRight.X)
            {
                var v = radii.LeftTop / (radii.LeftTop + radii.RightTop) * rect.Width;
                topLeft.X = v;
                topRight.X = v;
            }

            //  right edge
            if (rightTop.Y > rightBottom.Y)
            {
                var v = radii.TopRight / (radii.TopRight + radii.BottomRight) * rect.Height;
                rightTop.Y = v;
                rightBottom.Y = v;
            }

            //  bottom edge
            if (bottomRight.X < bottomLeft.X)
            {
                var v = radii.LeftBottom / (radii.LeftBottom + radii.RightBottom) * rect.Width;
                bottomRight.X = v;
                bottomLeft.X = v;
            }

            //  left edge
            if (leftBottom.Y < leftTop.Y)
            {
                var v = radii.TopLeft / (radii.TopLeft + radii.BottomLeft) * rect.Height;
                leftBottom.Y = v;
                leftTop.Y = v;
            }

            //  add on offsets
            var offset = new Vector(rect.TopLeft.X, rect.TopLeft.Y);
            topLeft += offset;
            topRight += offset;
            rightTop += offset;
            rightBottom += offset;
            bottomRight += offset;
            bottomLeft += offset;
            leftBottom += offset;
            leftTop += offset;

            //  create the border geometry
            ctx.BeginFigure(topLeft, true, true);

            //  Top line
            ctx.LineTo(topRight, true, false);

            //  Upper-right corner
            var radiusX = rect.TopRight.X - topRight.X;
            var radiusY = rightTop.Y - rect.TopRight.Y;
            if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY))
            {
                ctx.ArcTo(rightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
            }

            //  Right line
            ctx.LineTo(rightBottom, true, false);

            //  Lower-right corner
            radiusX = rect.BottomRight.X - bottomRight.X;
            radiusY = rect.BottomRight.Y - rightBottom.Y;
            if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY))
            {
                ctx.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
            }

            //  Bottom line
            ctx.LineTo(bottomLeft, true, false);

            //  Lower-left corner
            radiusX = bottomLeft.X - rect.BottomLeft.X;
            radiusY = rect.BottomLeft.Y - leftBottom.Y;
            if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY))
            {
                ctx.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
            }

            //  Left line
            ctx.LineTo(leftTop, true, false);

            //  Upper-left corner
            radiusX = topLeft.X - rect.TopLeft.X;
            radiusY = leftTop.Y - rect.TopLeft.Y;
            if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY))
            {
                ctx.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
            }
        }

        protected override Size MeasureOverride(Size constraint)
        {
            var child = Child;
            var borderThickness = BorderThickness;
            var padding = Padding;

            if (UseLayoutRounding)
            {
                var dpiScaleX = DpiHelper.DeviceDpiX;
                var dpiScaleY = DpiHelper.DeviceDpiY;

                borderThickness = new Thickness(
                    DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX),
                    DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY),
                    DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX),
                    DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY));
            }

            var borderSize = ConvertThickness2Size(borderThickness);
            var paddingSize = ConvertThickness2Size(padding);
            var mySize = new Size();

            if (child != null)
            {
                var combined = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height);
                var childConstraint = new Size(Math.Max(0.0, constraint.Width - combined.Width), Math.Max(0.0, constraint.Height - combined.Height));

                child.Measure(childConstraint);
                var childSize = child.DesiredSize;

                mySize.Width = childSize.Width + combined.Width;
                mySize.Height = childSize.Height + combined.Height;
            }
            else
            {
                mySize = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height);
            }

            return mySize;
        }

        protected override Size ArrangeOverride(Size arrangeSize)
        {
            var borderThickness = BorderThickness;

            if (UseLayoutRounding)
            {
                var dpiScaleX = DpiHelper.DeviceDpiX;
                var dpiScaleY = DpiHelper.DeviceDpiY;

                borderThickness = new Thickness(
                    DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX),
                    DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY),
                    DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX),
                    DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY));
            }

            var boundRect = new Rect(arrangeSize);
            var innerRect = DeflateRect(boundRect, borderThickness);

            var child = Child;
            if (child != null)
            {
                var padding = Padding;
                var childRect = DeflateRect(innerRect, padding);
                child.Arrange(childRect);
            }

            var radii = CornerRadius;
            var borderBrush = BorderBrush;
            var uniformCorners = AreUniformCorners(radii);

            _useComplexRenderCodePath = !uniformCorners;
            if (!_useComplexRenderCodePath && borderBrush != null)
            {
                _useComplexRenderCodePath = !MathHelper.IsZero(radii.TopLeft) && !borderThickness.IsUniform();
            }

            if (_useComplexRenderCodePath)
            {
                var innerRadii = new Radii(radii, borderThickness, false);
                StreamGeometry backgroundGeometry = null;

                if (!MathHelper.IsZero(innerRect.Width) && !MathHelper.IsZero(innerRect.Height))
                {
                    backgroundGeometry = new StreamGeometry();

                    using (var ctx = backgroundGeometry.Open())
                    {
                        GenerateGeometry(ctx, innerRect, innerRadii);
                    }

                    backgroundGeometry.Freeze();
                    BackgroundGeometryCache = backgroundGeometry;
                }
                else
                {
                    BackgroundGeometryCache = null;
                }

                if (!MathHelper.IsZero(boundRect.Width) && !MathHelper.IsZero(boundRect.Height))
                {
                    var outerRadii = new Radii(radii, borderThickness, true);
                    var borderGeometry = new StreamGeometry();

                    using (var ctx = borderGeometry.Open())
                    {
                        GenerateGeometry(ctx, boundRect, outerRadii);

                        if (backgroundGeometry != null)
                        {
                            GenerateGeometry(ctx, innerRect, innerRadii);
                        }
                    }

                    borderGeometry.Freeze();
                    BorderGeometryCache = borderGeometry;
                }
                else
                {
                    BorderGeometryCache = null;
                }
            }
            else
            {
                BackgroundGeometryCache = null;
                BorderGeometryCache = null;
            }

            return arrangeSize;
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
            var background = Background;
            var borderBrush = BorderBrush;
            var useLayoutRounding = UseLayoutRounding;

            if (_useComplexRenderCodePath)
            {
                var borderGeometry = BorderGeometryCache;
                if (borderGeometry != null && borderBrush != null)
                {
                    var pen = GeometryPenCache;
                    if (pen == null)
                    {
                        pen = new Pen
                        {
                            Brush = borderBrush,
                            Thickness = BorderDashThickness,
                            DashCap = BorderDashCap,
                            DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                        };

                        if (borderBrush.IsFrozen)
                        {
                            pen.Freeze();
                        }

                        GeometryPenCache = pen;
                    }

                    drawingContext.DrawGeometry(null, pen, borderGeometry);
                }

                var backgroundGeometry = BackgroundGeometryCache;
                if (backgroundGeometry != null && background != null)
                {
                    drawingContext.DrawGeometry(background, null, backgroundGeometry);
                }
            }
            else
            {
                var dpiScaleX = DpiHelper.DeviceDpiX;
                var dpiScaleY = DpiHelper.DeviceDpiY;

                var borderThickness = BorderThickness;
                var cornerRadius = CornerRadius;
                var outerCornerRadius = cornerRadius.TopLeft;
                var roundedCorners = !MathHelper.IsZero(outerCornerRadius);

                if (!borderThickness.IsZero() && borderBrush != null)
                {
                    var pen = LeftPenCache;
                    if (pen == null)
                    {
                        pen = new Pen
                        {
                            Brush = borderBrush,
                            Thickness = useLayoutRounding
                                ? DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX)
                                : borderThickness.Left,
                            DashCap = BorderDashCap,
                            DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                        };

                        if (borderBrush.IsFrozen)
                        {
                            pen.Freeze();
                        }

                        LeftPenCache = pen;
                    }

                    double halfThickness;
                    var renderSize = RenderSize;

                    if (borderThickness.IsUniform())
                    {
                        halfThickness = pen.Thickness * 0.5;
                        var rect = new Rect(new Point(halfThickness, halfThickness), new Point(renderSize.Width - halfThickness, renderSize.Height - halfThickness));

                        if (roundedCorners)
                        {
                            drawingContext.DrawRoundedRectangle(null, pen, rect, outerCornerRadius, outerCornerRadius);
                        }
                        else
                        {
                            drawingContext.DrawRectangle(null, pen, rect);
                        }
                    }
                    else
                    {
                        if (MathHelper.GreaterThan(borderThickness.Left, 0))
                        {
                            halfThickness = pen.Thickness * 0.5;
                            drawingContext.DrawLine(pen, new Point(halfThickness, 0), new Point(halfThickness, renderSize.Height));
                        }

                        if (MathHelper.GreaterThan(borderThickness.Right, 0))
                        {
                            pen = RightPenCache;
                            if (pen == null)
                            {
                                pen = new Pen
                                {
                                    Brush = borderBrush,
                                    Thickness = useLayoutRounding
                                        ? DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX)
                                        : borderThickness.Right,
                                    DashCap = BorderDashCap,
                                    DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                                };

                                if (borderBrush.IsFrozen)
                                {
                                    pen.Freeze();
                                }

                                RightPenCache = pen;
                            }

                            halfThickness = pen.Thickness * 0.5;
                            drawingContext.DrawLine(pen, new Point(renderSize.Width - halfThickness, 0), new Point(renderSize.Width - halfThickness, renderSize.Height));
                        }

                        if (MathHelper.GreaterThan(borderThickness.Top, 0))
                        {
                            pen = TopPenCache;
                            if (pen == null)
                            {
                                pen = new Pen
                                {
                                    Brush = borderBrush,
                                    Thickness = useLayoutRounding
                                        ? DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY)
                                        : borderThickness.Top,
                                    DashCap = BorderDashCap,
                                    DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                                };

                                if (borderBrush.IsFrozen)
                                {
                                    pen.Freeze();
                                }

                                TopPenCache = pen;
                            }

                            halfThickness = pen.Thickness * 0.5;
                            drawingContext.DrawLine(pen, new Point(0, halfThickness), new Point(renderSize.Width, halfThickness));
                        }

                        if (MathHelper.GreaterThan(borderThickness.Bottom, 0))
                        {
                            pen = BottomPenCache;
                            if (pen == null)
                            {
                                pen = new Pen
                                {
                                    Brush = borderBrush,
                                    Thickness = useLayoutRounding
                                        ? DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY)
                                        : borderThickness.Bottom,
                                    DashCap = BorderDashCap,
                                    DashStyle = new DashStyle(BorderDashArray, BorderDashOffset)
                                };

                                if (borderBrush.IsFrozen)
                                {
                                    pen.Freeze();
                                }

                                BottomPenCache = pen;
                            }

                            halfThickness = pen.Thickness * 0.5;
                            drawingContext.DrawLine(pen, new Point(0, renderSize.Height - halfThickness), new Point(renderSize.Width, renderSize.Height - halfThickness));
                        }
                    }
                }

                if (background != null)
                {
                    Point ptTL, ptBR;

                    if (useLayoutRounding)
                    {
                        ptTL = new Point(DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX), DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY));
                        ptBR = new Point(RenderSize.Width - DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX),
                            RenderSize.Height - DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY));
                    }
                    else
                    {
                        ptTL = new Point(borderThickness.Left, borderThickness.Top);
                        ptBR = new Point(RenderSize.Width - borderThickness.Right, RenderSize.Height - borderThickness.Bottom);
                    }

                    if (ptBR.X > ptTL.X && ptBR.Y > ptTL.Y)
                    {
                        if (roundedCorners)
                        {
                            var innerRadii = new Radii(cornerRadius, borderThickness, false);
                            var innerCornerRadius = innerRadii.TopLeft;
                            drawingContext.DrawRoundedRectangle(background, null, new Rect(ptTL, ptBR), innerCornerRadius, innerCornerRadius);
                        }
                        else
                        {
                            drawingContext.DrawRectangle(background, null, new Rect(ptTL, ptBR));
                        }
                    }
                }
            }
        }

        private readonly struct Radii
        {
            internal Radii(CornerRadius radii, Thickness borders, bool outer)
            {
                var left = 0.5 * borders.Left;
                var top = 0.5 * borders.Top;
                var right = 0.5 * borders.Right;
                var bottom = 0.5 * borders.Bottom;

                if (outer)
                {
                    if (MathHelper.IsZero(radii.TopLeft))
                    {
                        LeftTop = TopLeft = 0.0;
                    }
                    else
                    {
                        LeftTop = radii.TopLeft + left;
                        TopLeft = radii.TopLeft + top;
                    }
                    if (MathHelper.IsZero(radii.TopRight))
                    {
                        TopRight = RightTop = 0.0;
                    }
                    else
                    {
                        TopRight = radii.TopRight + top;
                        RightTop = radii.TopRight + right;
                    }
                    if (MathHelper.IsZero(radii.BottomRight))
                    {
                        RightBottom = BottomRight = 0.0;
                    }
                    else
                    {
                        RightBottom = radii.BottomRight + right;
                        BottomRight = radii.BottomRight + bottom;
                    }
                    if (MathHelper.IsZero(radii.BottomLeft))
                    {
                        BottomLeft = LeftBottom = 0.0;
                    }
                    else
                    {
                        BottomLeft = radii.BottomLeft + bottom;
                        LeftBottom = radii.BottomLeft + left;
                    }
                }
                else
                {
                    LeftTop = Math.Max(0.0, radii.TopLeft - left);
                    TopLeft = Math.Max(0.0, radii.TopLeft - top);
                    TopRight = Math.Max(0.0, radii.TopRight - top);
                    RightTop = Math.Max(0.0, radii.TopRight - right);
                    RightBottom = Math.Max(0.0, radii.BottomRight - right);
                    BottomRight = Math.Max(0.0, radii.BottomRight - bottom);
                    BottomLeft = Math.Max(0.0, radii.BottomLeft - bottom);
                    LeftBottom = Math.Max(0.0, radii.BottomLeft - left);
                }
            }

            internal readonly double LeftTop;
            internal readonly double TopLeft;
            internal readonly double TopRight;
            internal readonly double RightTop;
            internal readonly double RightBottom;
            internal readonly double BottomRight;
            internal readonly double BottomLeft;
            internal readonly double LeftBottom;
        }
    }
}
