using Unity.Collections;
using Unity.Mathematics;
using Unity.Collections.LowLevel.Unsafe;

namespace UnityEngine.U2D.Common.UTess
{

    // Ensures that there are no duplicate Points, Overlapping Edges.
    struct PlanarGraph
    {

        private static readonly double kEpsilon = 0.00001;
        private static readonly int kMaxIntersectionTolerance = 4;  // Maximum Intersection Tolerance per Intersection Loop Check.

        internal static void RemoveDuplicateEdges(ref Array<int2> edges, ref int edgeCount, Array<int> duplicates, int duplicateCount)
        {

            if (duplicateCount == 0)
            {
                for (var i = 0; i < edgeCount; ++i)
                {
                    var e = edges[i];
                    e.x = math.min(edges[i].x, edges[i].y);
                    e.y = math.max(edges[i].x, edges[i].y);
                    edges[i] = e;
                }
            }
            else
            {
                for (var i = 0; i < edgeCount; ++i)
                {
                    var e = edges[i];
                    var a = duplicates[e.x];
                    var b = duplicates[e.y];
                    e.x = math.min(a, b);
                    e.y = math.max(a, b);
                    edges[i] = e;
                }
            }

            unsafe
            {
                ModuleHandle.InsertionSort<int2, TessEdgeCompare>(edges.UnsafePtr, 0, edgeCount - 1,new TessEdgeCompare());
            }

            var n = 1;
            for (var i = 1; i < edgeCount; ++i)
            {
                var prev = edges[i - 1];
                var next = edges[i];
                if (next.x == prev.x && next.y == prev.y)
                    continue;
                if (next.x == next.y)
                    continue;
                edges[n++] = next;
            }
            edgeCount = n;
        }

        internal static bool CheckCollinear(double2 a0, double2 a1, double2 b0, double2 b1)
        {
            double2 a = a0;
            double2 b = a1;
            double2 c = b0;
            double2 d = b1;

            double x = (b.y - a.y) / (b.x - a.x);
            double y = (c.y - a.y) / (c.x - a.x);
            double z = (d.y - a.y) / (d.x - a.x);
            return ((!math.isinf(x) || !math.isinf(y) || !math.isinf(z)) && math.abs(x - y) > kEpsilon && math.abs(x - z) > kEpsilon);
        }

        internal static bool LineLineIntersection(double2 a0, double2 a1, double2 b0, double2 b1)
        {
            var x0 = ModuleHandle.OrientFastDouble(a0, b0, b1);
            var y0 = ModuleHandle.OrientFastDouble(a1, b0, b1);
            if ((x0 > kEpsilon && y0 > kEpsilon) || (x0 < -kEpsilon && y0 < -kEpsilon))
            {
                return false;
            }

            var x1 = ModuleHandle.OrientFastDouble(b0, a0, a1);
            var y1 = ModuleHandle.OrientFastDouble(b1, a0, a1);
            if ((x1 > kEpsilon && y1 > kEpsilon) || (x1 < -kEpsilon && y1 < -kEpsilon))
            {
                return false;
            }

            // Check for degenerate collinear case
            if (math.abs(x0) < kEpsilon && math.abs(y0) < kEpsilon && math.abs(x1) < kEpsilon && math.abs(y1) < kEpsilon)
            {
                return CheckCollinear(a0, a1, b0, b1);
            }

            return true;
        }

        internal static bool LineLineIntersection(double2 p1, double2 p2, double2 p3, double2 p4, ref double2 result)
        {
            double bx = p2.x - p1.x;
            double by = p2.y - p1.y;
            double dx = p4.x - p3.x;
            double dy = p4.y - p3.y;
            double bDotDPerp = bx * dy - by * dx;
            if (math.abs(bDotDPerp) < kEpsilon)
            {
                return false;
            }

            double cx = p3.x - p1.x;
            double cy = p3.y - p1.y;
            double t = (cx * dy - cy * dx) / bDotDPerp;
            if ((t >= -kEpsilon) && (t <= 1.0f + kEpsilon))
            {
                result.x = p1.x + t * bx;
                result.y = p1.y + t * by;
                return true;
            }
            return false;
        }

        internal static bool CalculateEdgeIntersections(Array<int2> edges, int edgeCount, Array<double2> points, int pointCount, ref Array<int2> results, ref Array<double2> intersects, ref int resultCount)
        {
            resultCount = 0;

            for (int i = 0; i < edgeCount; ++i)
            {
                for (int j = i + 1; j < edgeCount; ++j)
                {
                    var e = edges[i];
                    var f = edges[j];
                    if (e.x == f.x || e.x == f.y || e.y == f.x || e.y == f.y)
                        continue;

                    var a = points[e.x];
                    var b = points[e.y];
                    var c = points[f.x];
                    var d = points[f.y];
                    var g = double2.zero;
                    if (LineLineIntersection(a, b, c, d))
                    {
                        if (LineLineIntersection(a, b, c, d, ref g))
                        {
                            // Until we ensure Outline is generated properly, we need this useless Check every correction.
                            if (resultCount >= intersects.Length)
                                return false;

                            intersects[resultCount] = g;
                            results[resultCount++] = new int2(i, j);
                        }
                    }
                }
            }

            // Basically we have self intersections all over (eg. gobo_tree_04). Better don't generate any Mesh as even though this will eventually succeed, error correction will take long time.
            if (resultCount > (edgeCount * kMaxIntersectionTolerance))
            {
                return false;
            }

            var tjc = new IntersectionCompare();
            tjc.edges = edges;
            tjc.points = points;
            unsafe
            {
                ModuleHandle.InsertionSort<int2, IntersectionCompare>(results.UnsafePtr, 0, resultCount - 1, tjc);
            }

            return true;
        }

        internal static bool CalculateTJunctions(Array<int2> edges, int edgeCount, Array<double2> points, int pointCount, Array<int2> results, ref int resultCount)
        {
            resultCount = 0;

            for (int i = 0; i < edgeCount; ++i)
            {
                for (int j = 0; j < pointCount; ++j)
                {
                    var e = edges[i];
                    if (e.x == j || e.y == j)
                        continue;

                    var a = points[e.x];
                    var b = points[e.y];
                    var c = points[j];
                    var d = points[j];
                    if (LineLineIntersection(a, b, c, d))
                    {
                        // Until we ensure Outline is generated properly, we need this useless Check every correction.
                        if (resultCount >= results.Length)
                            return false;
                        results[resultCount++] = new int2(i, j);
                    }
                }
            }

            return true;
        }

        internal static bool CutEdges(ref Array<double2> points, ref int pointCount, ref Array<int2> edges, ref int edgeCount, ref Array<int2> tJunctions, ref int tJunctionCount, Array<int2> intersections, Array<double2> intersects, int intersectionCount)
        {
            for (int i = 0; i < intersectionCount; ++i)
            {
                var intr = intersections[i];
                var e = intr.x;
                var f = intr.y;

                int2 j1 = int2.zero;
                j1.x = e;
                j1.y = pointCount;
                tJunctions[tJunctionCount++] = j1;
                int2 j2 = int2.zero;
                j2.x = f;
                j2.y = pointCount;
                tJunctions[tJunctionCount++] = j2;

                // Until we ensure Outline is generated properly, we need this useless Check every correction.
                if (pointCount >= points.Length)
                    return false;

                points[pointCount++] = intersects[i];
            }

            unsafe
            {
                ModuleHandle.InsertionSort<int2, TessJunctionCompare>( tJunctions.UnsafePtr, 0, tJunctionCount - 1, new TessJunctionCompare());
            }

            // Split edges along junctions
            for (int i = tJunctionCount - 1; i >= 0; --i)
            {
                var tJunction = tJunctions[i];
                var e = tJunction.x;
                var edge = edges[e];
                var s = edge.x;
                var t = edge.y;

                // Check if edge is not lexicographically sorted
                var a = points[s];
                var b = points[t];
                if (((a.x - b.x) < 0) || (a.x == b.x && (a.y - b.y) < 0))
                {
                    var tmp = s;
                    s = t;
                    t = tmp;
                }

                // Split leading edge
                edge.x = s;
                var last = edge.y = tJunction.y;
                edges[e] = edge;

                // If we are grouping edges by color, remember to track data
                // Split other edges
                while (i > 0 && tJunctions[i - 1].x == e)
                {
                    var next = tJunctions[--i].y;
                    int2 te = new int2();
                    te.x = last;
                    te.y = next;
                    edges[edgeCount++] = te;
                    last = next;
                }

                int2 le = new int2();
                le.x = last;
                le.y = t;
                edges[edgeCount++] = le;
            }

            return true;
        }

        internal static void RemoveDuplicatePoints(ref Array<double2> points, ref int pointCount, ref Array<int> duplicates, ref int duplicateCount, Allocator allocator)
        {
            TessLink link = TessLink.CreateLink(pointCount, allocator);

            for (int i = 0; i < pointCount; ++i)
            {
                for (int j = i + 1; j < pointCount; ++j)
                {
                    if (math.distance(points[i], points[j]) < kEpsilon)
                    {
                        link.Link(i, j);
                    }
                }
            }

            duplicateCount = 0;
            for (var i = 0; i < pointCount; ++i)
            {
                var j = link.Find(i);
                if (j != i)
                {
                    duplicateCount++;
                    points[j] = math.min(points[i], points[j]);
                }
            }

            // Find Duplicates.
            if (duplicateCount != 0)
            {

                var prevPointCount = pointCount;
                pointCount = 0;
                for (var i = 0; i < prevPointCount; ++i)
                {
                    var j = link.Find(i);
                    if (j == i)
                    {
                        duplicates[i] = pointCount;
                        points[pointCount++] = points[i];
                    }
                    else
                    {
                        duplicates[i] = -1;
                    }
                }

                // Update Duplicates.
                for (int i = 0; i < prevPointCount; ++i)
                {
                    if (duplicates[i] < 0)
                    {
                        duplicates[i] = duplicates[link.Find(i)];
                    }
                }

            }

            TessLink.DestroyLink(link);
        }

        // Validate the Input Points ane Edges.
        internal static bool Validate(Allocator allocator, in NativeArray<float2> inputPoints, int pointCount, in NativeArray<int2> inputEdges, int edgeCount, ref NativeArray<float2> outputPoints, out int outputPointCount, ref NativeArray<int2> outputEdges, out int outputEdgeCount)
        {
            outputPointCount = 0;
            outputEdgeCount = 0;
            
            // Outline generated inputs can have differences in the range of 0.00001f.. See TwoLayers.psb sample.
            // Since PlanarGraph operates on double, scaling up and down does not result in loss of data.
            var precisionFudge = 10000.0f;
            var protectLoop = edgeCount;
            var requiresFix = true;
            var validGraph = false;

            // Processing Arrays.
            int startEdgeCount = edgeCount;
            Array<int> duplicates = new Array<int>(startEdgeCount, ModuleHandle.kMaxEdgeCount, allocator, NativeArrayOptions.UninitializedMemory);
            Array<int2> edges = new Array<int2>(startEdgeCount, ModuleHandle.kMaxEdgeCount, allocator, NativeArrayOptions.UninitializedMemory);
            Array<int2> tJunctions = new Array<int2>(startEdgeCount, ModuleHandle.kMaxEdgeCount, allocator, NativeArrayOptions.UninitializedMemory);
            Array<int2> edgeIntersections = new Array<int2>(startEdgeCount, ModuleHandle.kMaxEdgeCount, allocator, NativeArrayOptions.UninitializedMemory);
            Array<double2> points = new Array<double2>(pointCount * 2, pointCount * 8, allocator, NativeArrayOptions.UninitializedMemory);
            Array<double2> intersects = new Array<double2>(pointCount * 2, pointCount * 8, allocator, NativeArrayOptions.UninitializedMemory);

            // Initialize.
            for (int i = 0; i < pointCount; ++i)
                points[i] = inputPoints[i] * precisionFudge;
            unsafe
            {
                UnsafeUtility.MemCpy(edges.UnsafeReadOnlyPtr, inputEdges.GetUnsafePtr(), edgeCount * sizeof(int2));
            }

            // Pre-clear duplicates, otherwise the following will simply fail.
            RemoveDuplicateEdges(ref edges, ref edgeCount, duplicates, 0);

            // While PSG is clean.
            while (requiresFix && --protectLoop > 0)
            {
                // Edge Edge Intersection.
                int intersectionCount = 0;
                validGraph = CalculateEdgeIntersections(edges, edgeCount, points, pointCount, ref edgeIntersections, ref intersects, ref intersectionCount);
                if (!validGraph)
                    break;

                // Edge Point Intersection. T-Junction.
                int tJunctionCount = 0;
                validGraph = CalculateTJunctions(edges, edgeCount, points, pointCount, tJunctions, ref tJunctionCount);
                if (!validGraph)
                    break;

                // Cut Overlapping Edges.
                validGraph = CutEdges(ref points, ref pointCount, ref edges, ref edgeCount, ref tJunctions, ref tJunctionCount, edgeIntersections, intersects, intersectionCount);
                if (!validGraph)
                    break;

                // Remove Duplicate Points.
                int duplicateCount = 0;
                RemoveDuplicatePoints(ref points, ref pointCount, ref duplicates, ref duplicateCount, allocator);
                RemoveDuplicateEdges(ref edges, ref edgeCount, duplicates, duplicateCount);

                requiresFix = intersectionCount != 0 || tJunctionCount != 0;
            }

            if (validGraph)
            {
                // Finalize Output.
                outputEdgeCount = edgeCount;
                outputPointCount = pointCount;
                unsafe
                {
                    UnsafeUtility.MemCpy(outputEdges.GetUnsafePtr(), edges.UnsafeReadOnlyPtr, edgeCount * sizeof(int2));
                }
                for (int i = 0; i < pointCount; ++i)
                    outputPoints[i] = new float2((float)(points[i].x / precisionFudge), (float)(points[i].y / precisionFudge));
            }

            edges.Dispose();
            points.Dispose();
            intersects.Dispose();
            duplicates.Dispose();
            tJunctions.Dispose();
            edgeIntersections.Dispose();

            return (validGraph && protectLoop > 0);
        }

    }

}