using Avalonia;
using Avalonia.Media;
using Avalonia.Utilities;

namespace AtomUI.Controls.Utils;

/// <summary>
/// Contains internal helpers used to build and draw various geometries.
/// </summary>
internal class RoundRectGeometryBuilder
{
    private const double PiOver2 = 1.57079633; // 90 deg to rad
    private const double Epsilon = 0.00000153; // Same as LayoutHelper.LayoutEpsilon

    /// <summary>
    /// Draws a new rounded rectangle within the given geometry context.
    /// Warning: The caller must manage and dispose the <see cref="StreamGeometryContext" /> externally.
    /// </summary>
    /// <remarks>
    /// WinUI:
    /// https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L1072-L1079
    /// </remarks>
    /// <param name="context">The geometry context to draw into.</param>
    /// <param name="keypoints">The rounded rectangle keypoints defining the rectangle to draw.</param>
    public static void DrawRoundedCornersRectangle(
        StreamGeometryContext context,
        ref RoundedRectKeypoints keypoints)
    {
        double radiusX;
        double radiusY;

        context.BeginFigure(keypoints.TopLeft, true);

        // Top
        context.LineTo(keypoints.TopRight);

        // TopRight corner
        radiusX = keypoints.RightTop.X - keypoints.TopRight.X;
        radiusY = keypoints.TopRight.Y - keypoints.RightTop.Y;
        radiusX = radiusX > 0 ? radiusX : -radiusX;
        radiusY = radiusY > 0 ? radiusY : -radiusY;

        context.ArcTo(
            keypoints.RightTop,
            new Size(radiusX, radiusY),
            0.0,
            false,
            SweepDirection.Clockwise);

        // Right
        context.LineTo(keypoints.RightBottom);

        // BottomRight corner
        radiusX = keypoints.RightBottom.X - keypoints.BottomRight.X;
        radiusY = keypoints.BottomRight.Y - keypoints.RightBottom.Y;
        radiusX = radiusX > 0 ? radiusX : -radiusX;
        radiusY = radiusY > 0 ? radiusY : -radiusY;

        if (radiusX != 0 || radiusY != 0)
        {
            context.ArcTo(
                keypoints.BottomRight,
                new Size(radiusX, radiusY),
                0.0,
                false,
                SweepDirection.Clockwise);
        }

        // Bottom
        context.LineTo(keypoints.BottomLeft);

        // BottomLeft corner
        radiusX = keypoints.BottomLeft.X - keypoints.LeftBottom.X;
        radiusY = keypoints.BottomLeft.Y - keypoints.LeftBottom.Y;
        radiusX = radiusX > 0 ? radiusX : -radiusX;
        radiusY = radiusY > 0 ? radiusY : -radiusY;

        if (radiusX != 0 || radiusY != 0)
        {
            context.ArcTo(
                keypoints.LeftBottom,
                new Size(radiusX, radiusY),
                0.0,
                false,
                SweepDirection.Clockwise);
        }

        // Left
        context.LineTo(keypoints.LeftTop);

        // TopLeft corner
        radiusX = keypoints.TopLeft.X - keypoints.LeftTop.X;
        radiusY = keypoints.TopLeft.Y - keypoints.LeftTop.Y;
        radiusX = radiusX > 0 ? radiusX : -radiusX;
        radiusY = radiusY > 0 ? radiusY : -radiusY;

        if (radiusX != 0 || radiusY != 0)
        {
            context.ArcTo(
                keypoints.TopLeft,
                new Size(radiusX, radiusY),
                0.0,
                false,
                SweepDirection.Clockwise);
        }

        context.EndFigure(true);
    }

    /// <summary>
    /// Draws a new rounded rectangle within the given geometry context.
    /// Warning: The caller must manage and dispose the <see cref="StreamGeometryContext" /> externally.
    /// </summary>
    /// <param name="context">The geometry context to draw into.</param>
    /// <param name="rect">The existing rectangle dimensions without corner radii.</param>
    /// <param name="radiusX">The radius on the X-axis used to round the corners of the rectangle.</param>
    /// <param name="radiusY">The radius on the Y-axis used to round the corners of the rectangle.</param>
    public static void DrawRoundedCornersRectangle(
        StreamGeometryContext context,
        Rect rect,
        double radiusX,
        double radiusY)
    {
        var arcSize = new Size(radiusX, radiusY);

        // The rectangle is constructed as follows:
        //
        //   (origin)
        //   Corner 4            Corner 1
        //   Top/Left  Line 1    Top/Right
        //      \_   __________   _/
        //          |          |
        //   Line 4 |          | Line 2
        //       _  |__________|  _
        //      /      Line 3      \
        //   Corner 3            Corner 2
        //   Bottom/Left         Bottom/Right
        //
        // - Lines 1,3 follow the deflated rectangle bounds minus RadiusX
        // - Lines 2,4 follow the deflated rectangle bounds minus RadiusY
        // - All corners are constructed using elliptical arcs 

        context.BeginFigure(new Point(rect.Left + radiusX, rect.Top), true);

        // Line 1 + Corner 1
        context.LineTo(new Point(rect.Right - radiusX, rect.Top));
        context.ArcTo(
            new Point(rect.Right, rect.Top + radiusY),
            arcSize,
            PiOver2,
            false,
            SweepDirection.Clockwise);

        // Line 2 + Corner 2
        context.LineTo(new Point(rect.Right, rect.Bottom - radiusY));
        context.ArcTo(
            new Point(rect.Right - radiusX, rect.Bottom),
            arcSize,
            PiOver2,
            false,
            SweepDirection.Clockwise);

        // Line 3 + Corner 3
        context.LineTo(new Point(rect.Left + radiusX, rect.Bottom));
        context.ArcTo(
            new Point(rect.Left, rect.Bottom - radiusY),
            arcSize,
            PiOver2,
            false,
            SweepDirection.Clockwise);

        // Line 4 + Corner 4
        context.LineTo(new Point(rect.Left, rect.Top + radiusY));
        context.ArcTo(
            new Point(rect.Left + radiusX, rect.Top),
            arcSize,
            PiOver2,
            false,
            SweepDirection.Clockwise);

        context.EndFigure(true);
    }

    /// <summary>
    /// Calculates the keypoints of a rounded rectangle based on the algorithm in WinUI.
    /// These keypoints may then be drawn or transformed into other types.
    /// </summary>
    /// <param name="outerBounds">
    /// The outer bounds of the rounded rectangle.
    /// This should be the overall bounds and size of the shape/control without any
    /// corner radii or border thickness adjustments.
    /// </param>
    /// <param name="borderThickness">The unadjusted border thickness of the rounded rectangle.</param>
    /// <param name="cornerRadius">
    /// The unadjusted corner radii of the rounded rectangle.
    /// The corner radius is defined to be the middle of the border stroke (center of the border).
    /// </param>
    /// <param name="sizing">The sizing mode used to calculate the final rounded rectangle size.</param>
    /// <returns>New rounded rectangle keypoints.</returns>
    public static RoundedRectKeypoints CalculateRoundedCornersRectangleWinUI(
        Rect outerBounds,
        Thickness borderThickness,
        CornerRadius cornerRadius,
        BackgroundSizing sizing)
    {
        // This was initially derived from WinUI:
        //  - CGeometryBuilder::CalculateRoundedCornersRectangle
        //    https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L862-L869
        //
        // It has been modified to accept a BackgroundSizing parameter directly as well
        // as to support BackgroundSizing.CenterBorder.
        //
        // Keep in mind:
        //   > In Xaml, the corner radius is defined to be the middle of the stroke
        //   > (i.e. half the border thickness extends to either side).

        bool fOuter;
        var  boundRect = outerBounds;

        if (sizing == BackgroundSizing.InnerBorderEdge)
        {
            boundRect = outerBounds.Deflate(borderThickness);
            fOuter    = false;
        }
        else if (sizing == BackgroundSizing.OuterBorderEdge)
        {
            fOuter = true;
        }
        else // CenterBorder
        {
            // This is a trick to support a 3rd state (CenterBorder) using the same WinUI-based algorithm.
            // The WinUI algorithm only supports the fOuter = True|False parameter.
            boundRect = outerBounds.Deflate(borderThickness * 0.5);
            fOuter    = false;
        }

        // Start of WinUI converted code
        // WinUI's Point struct fields can be modified directly, Avalonia's Point is read-only.
        // Therefore, we will use doubles for calculation so multiple Point structs aren't
        // required during calculations -- everything can be done with these double variables.
        double fLeftTop;
        double fLeftBottom;
        double fTopLeft;
        double fTopRight;
        double fRightTop;
        double fRightBottom;
        double fBottomLeft;
        double fBottomRight;

        double left;
        double right;
        double top;
        double bottom;

        // If the caller wants to take the border into account
        // initialize the borders variables
        if (borderThickness != default)
        {
            left   = 0.5 * borderThickness.Left;
            right  = 0.5 * borderThickness.Right;
            top    = 0.5 * borderThickness.Top;
            bottom = 0.5 * borderThickness.Bottom;
        }
        else
        {
            left   = 0.0;
            right  = 0.0;
            top    = 0.0;
            bottom = 0.0;
        }

        // The following if/else block initializes the variables
        // of which the points of the path will be created
        // In case of outer, add the border - if any.
        // Otherwise (inner rectangle) subtract the border - if any
        if (fOuter)
        {
            if (MathUtilities.AreClose(cornerRadius.TopLeft, 0.0, Epsilon))
            {
                fLeftTop = 0.0;
                fTopLeft = 0.0;
            }
            else
            {
                fLeftTop = cornerRadius.TopLeft + left;
                fTopLeft = cornerRadius.TopLeft + top;
            }

            if (MathUtilities.AreClose(cornerRadius.TopRight, 0.0, Epsilon))
            {
                fTopRight = 0.0;
                fRightTop = 0.0;
            }
            else
            {
                fTopRight = cornerRadius.TopRight + top;
                fRightTop = cornerRadius.TopRight + right;
            }

            if (MathUtilities.AreClose(cornerRadius.BottomRight, 0.0, Epsilon))
            {
                fRightBottom = 0.0;
                fBottomRight = 0.0;
            }
            else
            {
                fRightBottom = cornerRadius.BottomRight + right;
                fBottomRight = cornerRadius.BottomRight + bottom;
            }

            if (MathUtilities.AreClose(cornerRadius.BottomLeft, 0.0, Epsilon))
            {
                fBottomLeft = 0.0;
                fLeftBottom = 0.0;
            }
            else
            {
                fBottomLeft = cornerRadius.BottomLeft + bottom;
                fLeftBottom = cornerRadius.BottomLeft + left;
            }
        }
        else
        {
            fLeftTop     = Math.Max(0.0, cornerRadius.TopLeft - left);
            fTopLeft     = Math.Max(0.0, cornerRadius.TopLeft - top);
            fTopRight    = Math.Max(0.0, cornerRadius.TopRight - top);
            fRightTop    = Math.Max(0.0, cornerRadius.TopRight - right);
            fRightBottom = Math.Max(0.0, cornerRadius.BottomRight - right);
            fBottomRight = Math.Max(0.0, cornerRadius.BottomRight - bottom);
            fBottomLeft  = Math.Max(0.0, cornerRadius.BottomLeft - bottom);
            fLeftBottom  = Math.Max(0.0, cornerRadius.BottomLeft - left);
        }

        var    topLeftX = fLeftTop;
        double topLeftY = 0;

        var    topRightX = boundRect.Width - fRightTop;
        double topRightY = 0;

        var rightTopX = boundRect.Width;
        var rightTopY = fTopRight;

        var rightBottomX = boundRect.Width;
        var rightBottomY = boundRect.Height - fBottomRight;

        var bottomRightX = boundRect.Width - fRightBottom;
        var bottomRightY = boundRect.Height;

        var bottomLeftX = fLeftBottom;
        var bottomLeftY = boundRect.Height;

        double leftBottomX = 0;
        var    leftBottomY = boundRect.Height - fBottomLeft;

        double leftTopX = 0;
        var    leftTopY = fTopLeft;

        // check keypoints for overlap and resolve by partitioning radii according to
        // the percentage of each one.

        // top edge
        if (topLeftX > topRightX)
        {
            var v = fLeftTop / (fLeftTop + fRightTop) * boundRect.Width;
            topLeftX  = v;
            topRightX = v;
        }

        // right edge
        if (rightTopY > rightBottomY)
        {
            var v = fTopRight / (fTopRight + fBottomRight) * boundRect.Height;
            rightTopY    = v;
            rightBottomY = v;
        }

        // bottom edge
        if (bottomRightX < bottomLeftX)
        {
            var v = fLeftBottom / (fLeftBottom + fRightBottom) * boundRect.Width;
            bottomRightX = v;
            bottomLeftX  = v;
        }

        // left edge
        if (leftBottomY < leftTopY)
        {
            var v = fTopLeft / (fTopLeft + fBottomLeft) * boundRect.Height;
            leftBottomY = v;
            leftTopY    = v;
        }

        // The above code does all calculations without taking into consideration X/Y absolute position.
        // In WinUI, this is compensated for in DrawRoundedCornersRectangle(); however, we do this here directly
        // when the final keypoints are being created.
        var keypoints = new RoundedRectKeypoints();
        keypoints.TopLeft = new Point(
            boundRect.X + topLeftX,
            boundRect.Y + topLeftY);
        keypoints.TopRight = new Point(
            boundRect.X + topRightX,
            boundRect.Y + topRightY);

        keypoints.RightTop = new Point(
            boundRect.X + rightTopX,
            boundRect.Y + rightTopY);
        keypoints.RightBottom = new Point(
            boundRect.X + rightBottomX,
            boundRect.Y + rightBottomY);

        keypoints.BottomRight = new Point(
            boundRect.X + bottomRightX,
            boundRect.Y + bottomRightY);
        keypoints.BottomLeft = new Point(
            boundRect.X + bottomLeftX,
            boundRect.Y + bottomLeftY);

        keypoints.LeftBottom = new Point(
            boundRect.X + leftBottomX,
            boundRect.Y + leftBottomY);
        keypoints.LeftTop = new Point(
            boundRect.X + leftTopX,
            boundRect.Y + leftTopY);

        return keypoints;
    }

    /// <summary>
    /// Represents the keypoints of a rounded rectangle.
    /// These keypoints can be shared between methods and turned into geometry.
    /// </summary>
    /// <remarks>
    /// A rounded rectangle is the base geometric shape used when drawing borders.
    /// It is a superset of a simple rectangle (which has corner radii set to zero).
    /// These keypoints can be combined together to produce geometries for both background
    /// and border elements.
    /// </remarks>
    internal struct RoundedRectKeypoints
    {
        // The following keypoints are defined for a rounded rectangle:
        //
        //       TopLeft                                  TopRight
        //              *--------------------------------*
        // (start)     /                                  \
        //    LeftTop *                                    * RightTop
        //            |                                    |
        //            |                                    |
        // LeftBottom *                                    * RightBottom
        //             \                                  /
        //              *--------------------------------*
        //    BottomLeft                                  BottomRight
        //
        // Or, for a simple rectangle without corner radii:
        //
        //    TopLeft = LeftTop                   TopRight = RightTop
        //  (start)   *------------------------------------*
        //            |                                    |
        //            |                                    |
        //            *------------------------------------*
        // BottomLeft = LeftBottom             BottomRight = RightBottom

        /// <summary>
        /// Initializes a new instance of the <see cref="RoundedRectKeypoints" /> struct.
        /// </summary>
        public RoundedRectKeypoints()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RoundedRectKeypoints" /> struct.
        /// </summary>
        /// <param name="roundedRect">An existing <see cref="RoundedRect" /> to initialize keypoints with.</param>
        public RoundedRectKeypoints(RoundedRect roundedRect)
        {
            LeftTop = new Point(
                roundedRect.Rect.TopLeft.X,
                roundedRect.Rect.TopLeft.Y + roundedRect.RadiiTopLeft.Y);
            TopLeft = new Point(
                roundedRect.Rect.TopLeft.X + roundedRect.RadiiTopLeft.X,
                roundedRect.Rect.TopLeft.Y);
            TopRight = new Point(
                roundedRect.Rect.TopRight.X - roundedRect.RadiiTopRight.X,
                roundedRect.Rect.TopRight.Y);
            RightTop = new Point(
                roundedRect.Rect.TopRight.X,
                roundedRect.Rect.TopRight.Y + roundedRect.RadiiTopRight.Y);
            RightBottom = new Point(
                roundedRect.Rect.BottomRight.X,
                roundedRect.Rect.BottomRight.Y - roundedRect.RadiiBottomRight.Y);
            BottomRight = new Point(
                roundedRect.Rect.BottomRight.X - roundedRect.RadiiBottomRight.X,
                roundedRect.Rect.BottomRight.Y);
            BottomLeft = new Point(
                roundedRect.Rect.BottomLeft.X + roundedRect.RadiiBottomLeft.X,
                roundedRect.Rect.BottomLeft.Y);
            LeftBottom = new Point(
                roundedRect.Rect.BottomLeft.X,
                roundedRect.Rect.BottomRight.Y - roundedRect.RadiiBottomLeft.Y);
        }

        /// <summary>
        /// Gets the topmost point in the left line segment of the rectangle.
        /// </summary>
        public Point LeftTop { get; set; }

        /// <summary>
        /// Gets the leftmost point in the top line segment of the rectangle.
        /// </summary>
        public Point TopLeft { get; set; }

        /// <summary>
        /// Gets the rightmost point in the top line segment of the rectangle.
        /// </summary>
        public Point TopRight { get; set; }

        /// <summary>
        /// Gets the topmost point in the right line segment of the rectangle.
        /// </summary>
        public Point RightTop { get; set; }

        /// <summary>
        /// Gets the bottommost point in the right line segment of the rectangle.
        /// </summary>
        public Point RightBottom { get; set; }

        /// <summary>
        /// Gets the rightmost point in the bottom line segment of the rectangle.
        /// </summary>
        public Point BottomRight { get; set; }

        /// <summary>
        /// Gets the leftmost point in the bottom line segment of the rectangle.
        /// </summary>
        public Point BottomLeft { get; set; }

        /// <summary>
        /// Gets the bottommost point in the left line segment of the rectangle.
        /// </summary>
        public Point LeftBottom { get; set; }

        /// <summary>
        /// Gets a value indicating whether the rounded rectangle is actually rounded on
        /// any corner. If false the key points represent a simple rectangle.
        /// </summary>
        public bool IsRounded =>
            TopLeft != LeftTop ||
            TopRight != RightTop ||
            BottomLeft != LeftBottom ||
            BottomRight != RightBottom;

        /// <summary>
        /// Converts the keypoints into a simple rectangle (with no corners).
        /// This is equivalent to the outer rectangle with zero corner radii.
        /// </summary>
        /// <remarks>
        /// Warning: This will force the keypoints into a simple rectangle without
        /// any rounded corners. Use <see cref="IsRounded" /> to determine if corner
        /// information is otherwise available.
        /// </remarks>
        /// <returns>A new rectangle representing the keypoints.</returns>
        public Rect ToRect()
        {
            return new Rect(
                new Point(
                    LeftTop.X,
                    TopLeft.Y),
                new Point(
                    RightBottom.X,
                    BottomRight.Y));
        }

        /// <summary>
        /// Converts the keypoints into a rounded rectangle with elliptical corner radii.
        /// </summary>
        /// <remarks>
        /// Elliptical corner radius (represented by <see cref="Vector" />) is more powerful
        /// than circular corner radius (represented by a <see cref="CornerRadius" />).
        /// Elliptical is a superset of circular.
        /// </remarks>
        /// <returns>A new rounded rectangle representing the keypoints.</returns>
        public RoundedRect ToRoundedRect()
        {
            return new RoundedRect(
                ToRect(),
                new Vector(
                    TopLeft.X - LeftTop.X,
                    LeftTop.Y - TopLeft.Y),
                new Vector(
                    RightTop.X - TopRight.X,
                    RightTop.Y - TopRight.Y),
                new Vector(
                    RightBottom.X - BottomRight.X,
                    BottomRight.Y - RightBottom.Y),
                new Vector(
                    BottomLeft.X - LeftBottom.X,
                    BottomLeft.Y - LeftBottom.Y));
        }
    }
}