// -----------------------------------------------------------------------
// <copyright file="TriangleQuadTree.cs" company="">
// Original code by Frank Dockhorn, [not available anymore: http://sourceforge.net/projects/quadtreesim/]
// Triangle.NET code by Christian Woltering, http://triangle.codeplex.com/
// </copyright>
// -----------------------------------------------------------------------

namespace UnityEngine.U2D.Animation.TriangleNet
    .Tools
{
    using System.Collections.Generic;
    using System.Linq;
    using Animation.TriangleNet.Geometry;

    /// <summary>
    /// A Quadtree implementation optimized for triangles.
    /// </summary>
    internal class TriangleQuadTree
    {
        QuadNode root;

        internal ITriangle[] triangles;

        internal int sizeBound;
        internal int maxDepth;

        /// <summary>
        /// Initializes a new instance of the <see cref="TriangleQuadTree" /> class.
        /// </summary>
        /// <param name="mesh">Mesh containing triangles.</param>
        /// <param name="maxDepth">The maximum depth of the tree.</param>
        /// <param name="sizeBound">The maximum number of triangles contained in a leaf.</param>
        /// <remarks>
        /// The quadtree does not track changes of the mesh. If a mesh is refined or
        /// changed in any other way, a new quadtree has to be built to make the point
        /// location work.
        ///
        /// A node of the tree will be split, if its level if less than the max depth parameter
        /// AND the number of triangles in the node is greater than the size bound.
        /// </remarks>
        public TriangleQuadTree(Mesh mesh, int maxDepth = 10, int sizeBound = 10)
        {
            this.maxDepth = maxDepth;
            this.sizeBound = sizeBound;

            triangles = mesh.Triangles.ToArray();

            int currentDepth = 0;

            root = new QuadNode(mesh.Bounds, this, true);
            root.CreateSubRegion(++currentDepth);
        }

        public ITriangle Query(double x, double y)
        {
            var point = new Point(x, y);
            var indices = root.FindTriangles(point);

            foreach (var i in indices)
            {
                var tri = this.triangles[i];

                if (IsPointInTriangle(point, tri.GetVertex(0), tri.GetVertex(1), tri.GetVertex(2)))
                {
                    return tri;
                }
            }

            return null;
        }

        /// <summary>
        /// Test, if a given point lies inside a triangle.
        /// </summary>
        /// <param name="p">Point to locate.</param>
        /// <param name="t0">Corner point of triangle.</param>
        /// <param name="t1">Corner point of triangle.</param>
        /// <param name="t2">Corner point of triangle.</param>
        /// <returns>True, if point is inside or on the edge of this triangle.</returns>
        internal static bool IsPointInTriangle(Point p, Point t0, Point t1, Point t2)
        {
            // TODO: no need to create new Point instances here
            Point d0 = new Point(t1.x - t0.x, t1.y - t0.y);
            Point d1 = new Point(t2.x - t0.x, t2.y - t0.y);
            Point d2 = new Point(p.x - t0.x, p.y - t0.y);

            // crossproduct of (0, 0, 1) and d0
            Point c0 = new Point(-d0.y, d0.x);

            // crossproduct of (0, 0, 1) and d1
            Point c1 = new Point(-d1.y, d1.x);

            // Linear combination d2 = s * d0 + v * d1.
            //
            // Multiply both sides of the equation with c0 and c1
            // and solve for s and v respectively
            //
            // s = d2 * c1 / d0 * c1
            // v = d2 * c0 / d1 * c0

            double s = DotProduct(d2, c1) / DotProduct(d0, c1);
            double v = DotProduct(d2, c0) / DotProduct(d1, c0);

            if (s >= 0 && v >= 0 && ((s + v) <= 1))
            {
                // Point is inside or on the edge of this triangle.
                return true;
            }

            return false;
        }

        internal static double DotProduct(Point p, Point q)
        {
            return p.x * q.x + p.y * q.y;
        }

        /// <summary>
        /// A node of the quadtree.
        /// </summary>
        class QuadNode
        {
            const int SW = 0;
            const int SE = 1;
            const int NW = 2;
            const int NE = 3;

            const double EPS = 1e-6;

            static readonly byte[] BITVECTOR = { 0x1, 0x2, 0x4, 0x8 };

            Rectangle bounds;
            Point pivot;
            TriangleQuadTree tree;
            QuadNode[] regions;
            List<int> triangles;

            byte bitRegions;

            public QuadNode(Rectangle box, TriangleQuadTree tree)
                : this(box, tree, false)
            {
            }

            public QuadNode(Rectangle box, TriangleQuadTree tree, bool init)
            {
                this.tree = tree;

                this.bounds = new Rectangle(box.Left, box.Bottom, box.Width, box.Height);
                this.pivot = new Point((box.Left + box.Right) / 2, (box.Bottom + box.Top) / 2);

                this.bitRegions = 0;

                this.regions = new QuadNode[4];
                this.triangles = new List<int>();

                if (init)
                {
                    int count = tree.triangles.Length;

                    // Allocate memory upfront
                    triangles.Capacity = count;

                    for (int i = 0; i < count; i++)
                    {
                        triangles.Add(i);
                    }
                }
            }

            public List<int> FindTriangles(Point searchPoint)
            {
                int region = FindRegion(searchPoint);
                if (regions[region] == null)
                {
                    return triangles;
                }
                return regions[region].FindTriangles(searchPoint);
            }

            public void CreateSubRegion(int currentDepth)
            {
                // The four sub regions of the quad tree
                //   +--------------+
                //   | nw 2 | ne 3  |
                //   |------+pivot--|
                //   | sw 0 | se 1  |
                //   +--------------+
                Rectangle box;

                var width = bounds.Right - pivot.x;
                var height = bounds.Top - pivot.y;

                // 1. region south west
                box = new Rectangle(bounds.Left, bounds.Bottom, width, height);
                regions[0] = new QuadNode(box, tree);

                // 2. region south east
                box = new Rectangle(pivot.x, bounds.Bottom, width, height);
                regions[1] = new QuadNode(box, tree);

                // 3. region north west
                box = new Rectangle(bounds.Left, pivot.y, width, height);
                regions[2] = new QuadNode(box, tree);

                // 4. region north east
                box = new Rectangle(pivot.x, pivot.y, width, height);
                regions[3] = new QuadNode(box, tree);

                Point[] triangle = new Point[3];

                // Find region for every triangle vertex
                foreach (var index in triangles)
                {
                    ITriangle tri = tree.triangles[index];

                    triangle[0] = tri.GetVertex(0);
                    triangle[1] = tri.GetVertex(1);
                    triangle[2] = tri.GetVertex(2);

                    AddTriangleToRegion(triangle, index);
                }

                for (int i = 0; i < 4; i++)
                {
                    if (regions[i].triangles.Count > tree.sizeBound && currentDepth < tree.maxDepth)
                    {
                        regions[i].CreateSubRegion(currentDepth + 1);
                    }
                }
            }

            void AddTriangleToRegion(Point[] triangle, int index)
            {
                bitRegions = 0;
                if (TriangleQuadTree.IsPointInTriangle(pivot, triangle[0], triangle[1], triangle[2]))
                {
                    AddToRegion(index, SW);
                    AddToRegion(index, SE);
                    AddToRegion(index, NW);
                    AddToRegion(index, NE);
                    return;
                }

                FindTriangleIntersections(triangle, index);

                if (bitRegions == 0)
                {
                    // we didn't find any intersection so we add this triangle to a point's region
                    int region = FindRegion(triangle[0]);
                    regions[region].triangles.Add(index);
                }
            }

            void FindTriangleIntersections(Point[] triangle, int index)
            {
                // PLEASE NOTE:
                // Handling of component comparison is tightly associated with the implementation
                // of the findRegion() function. That means when the point to be compared equals
                // the pivot point the triangle must be put at least into region 2.
                //
                // Linear equations are in parametric form.
                //    pivot.x = triangle[0].x + t * (triangle[1].x - triangle[0].x)
                //    pivot.y = triangle[0].y + t * (triangle[1].y - triangle[0].y)

                int k = 2;

                double dx, dy;
                // Iterate through all triangle laterals and find bounding box intersections
                for (int i = 0; i < 3; k = i++)
                {
                    dx = triangle[i].x - triangle[k].x;
                    dy = triangle[i].y - triangle[k].y;

                    if (dx != 0.0)
                    {
                        FindIntersectionsWithX(dx, dy, triangle, index, k);
                    }
                    if (dy != 0.0)
                    {
                        FindIntersectionsWithY(dx, dy, triangle, index, k);
                    }
                }
            }

            void FindIntersectionsWithX(double dx, double dy, Point[] triangle, int index, int k)
            {
                double t;

                // find intersection with plane x = m_pivot.dX
                t = (pivot.x - triangle[k].x) / dx;
                if (t < (1 + EPS) && t > -EPS)
                {
                    // we have an intersection
                    double yComponent = triangle[k].y + t * dy;

                    if (yComponent < pivot.y && yComponent >= bounds.Bottom)
                    {
                        AddToRegion(index, SW);
                        AddToRegion(index, SE);
                    }
                    else if (yComponent <= bounds.Top)
                    {
                        AddToRegion(index, NW);
                        AddToRegion(index, NE);
                    }
                }

                // find intersection with plane x = m_boundingBox[0].dX
                t = (bounds.Left - triangle[k].x) / dx;
                if (t < (1 + EPS) && t > -EPS)
                {
                    // we have an intersection
                    double yComponent = triangle[k].y + t * dy;

                    if (yComponent < pivot.y && yComponent >= bounds.Bottom)
                    {
                        AddToRegion(index, SW);
                    }
                    else if (yComponent <= bounds.Top) // TODO: check && yComponent >= pivot.Y
                    {
                        AddToRegion(index, NW);
                    }
                }

                // find intersection with plane x = m_boundingBox[1].dX
                t = (bounds.Right - triangle[k].x) / dx;
                if (t < (1 + EPS) && t > -EPS)
                {
                    // we have an intersection
                    double yComponent = triangle[k].y + t * dy;

                    if (yComponent < pivot.y && yComponent >= bounds.Bottom)
                    {
                        AddToRegion(index, SE);
                    }
                    else if (yComponent <= bounds.Top)
                    {
                        AddToRegion(index, NE);
                    }
                }
            }

            void FindIntersectionsWithY(double dx, double dy, Point[] triangle, int index, int k)
            {
                double t, xComponent;

                // find intersection with plane y = m_pivot.dY
                t = (pivot.y - triangle[k].y) / dy;
                if (t < (1 + EPS) && t > -EPS)
                {
                    // we have an intersection
                    xComponent = triangle[k].x + t * dx;

                    if (xComponent > pivot.x && xComponent <= bounds.Right)
                    {
                        AddToRegion(index, SE);
                        AddToRegion(index, NE);
                    }
                    else if (xComponent >= bounds.Left)
                    {
                        AddToRegion(index, SW);
                        AddToRegion(index, NW);
                    }
                }

                // find intersection with plane y = m_boundingBox[0].dY
                t = (bounds.Bottom - triangle[k].y) / dy;
                if (t < (1 + EPS) && t > -EPS)
                {
                    // we have an intersection
                    xComponent = triangle[k].x + t * dx;

                    if (xComponent > pivot.x && xComponent <= bounds.Right)
                    {
                        AddToRegion(index, SE);
                    }
                    else if (xComponent >= bounds.Left)
                    {
                        AddToRegion(index, SW);
                    }
                }

                // find intersection with plane y = m_boundingBox[1].dY
                t = (bounds.Top - triangle[k].y) / dy;
                if (t < (1 + EPS) && t > -EPS)
                {
                    // we have an intersection
                    xComponent = triangle[k].x + t * dx;

                    if (xComponent > pivot.x && xComponent <= bounds.Right)
                    {
                        AddToRegion(index, NE);
                    }
                    else if (xComponent >= bounds.Left)
                    {
                        AddToRegion(index, NW);
                    }
                }
            }

            int FindRegion(Point point)
            {
                int b = 2;
                if (point.y < pivot.y)
                {
                    b = 0;
                }
                if (point.x > pivot.x)
                {
                    b++;
                }
                return b;
            }

            void AddToRegion(int index, int region)
            {
                //if (!(m_bitRegions & BITVECTOR[region]))
                if ((bitRegions & BITVECTOR[region]) == 0)
                {
                    regions[region].triangles.Add(index);
                    bitRegions |= BITVECTOR[region];
                }
            }
        }
    }
}
