using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Linq;

namespace OpenNest.Geometry
{
    public class Polygon : Entity
    {
        public List<Vector> Vertices;

        public Polygon()
        {
            Vertices = new List<Vector>();
        }

        /// <summary>
        /// Closes the polygon if it's not already.
        /// </summary>
        public void Close()
        {
            if (Vertices.Count < 3)
                return;

            var first = Vertices.First();
            var last = Vertices.Last();

            if (first != last)
                Vertices.Add(first);
        }

        /// <summary>
        /// Returns true if the polygon is closed.
        /// </summary>
        /// <returns></returns>
        public bool IsClosed()
        {
            if (Vertices.Count < 3)
                return false;

            return (Vertices.First() == Vertices.Last());
        }

        /// <summary>
        /// Returns true if the polygon is self intersecting.
        /// </summary>
        /// <returns></returns>
        public bool IsComplex()
        {
            var lines = ToLines();

            for (int i = 0; i < lines.Count; ++i)
            {
                var line1 = lines[i];

                for (int j = i; j < lines.Count; ++j)
                {
                    var line2 = lines[j];

                    if (line1.Intersects(line2))
                        return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Area of the polygon.
        /// </summary>
        /// <returns>Returns the area or 0 if the polygon is NOT closed.</returns>
        public double Area()
        {
            if (Vertices.Count < 3)
                return 0.0;

            return System.Math.Abs(CalculateArea());
        }

        /// <summary>
        /// Distance around the polygon.
        /// </summary>
        /// <returns></returns>
        public double Perimeter()
        {
            if (Vertices.Count < 3)
                return 0.0;

            double sum = 0.0;

            var last = Vertices[0];

            for (int i = 1; i < Vertices.Count; ++i)
            {
                var current = Vertices[i];
                sum += last.DistanceTo(current);
                last = current;
            }

            return sum;
        }

        /// <summary>
        /// Gets the rotation direction of the polygon.
        /// </summary>
        /// <returns></returns>
        public RotationType RotationDirection()
        {
            if (Vertices.Count < 3)
                throw new Exception("Not enough points to determine direction. Must have at least 3 points.");

            return CalculateArea() > 0 ? RotationType.CCW : RotationType.CW;
        }

        /// <summary>
        /// Converts the polygon to a group of lines.
        /// </summary>
        /// <returns></returns>
        public List<Line> ToLines()
        {
            var list = new List<Line>();

            if (Vertices.Count < 2)
                return list;

            var last = Vertices[0];

            for (int i = 1; i < Vertices.Count; ++i)
            {
                var current = Vertices[i];
                list.Add(new Line(last, current));
                last = current;
            }

            return list;
        }

        /// <summary>
        /// Gets the area of the polygon.
        /// </summary>
        /// <returns>
        /// Returns the area of the polygon.
        /// * Positive number = counter-clockwise rotation
        /// * Negative number = clockwise rotation
        /// </returns>
        private double CalculateArea()
        {
            double xsum = 0;
            double ysum = 0;

            for (int i = 0; i < Vertices.Count - 1; ++i)
            {
                var current = Vertices[i];
                var next = Vertices[i + 1];

                xsum += current.X * next.Y;
                ysum += current.Y * next.X;
            }

            return (xsum - ysum) * 0.5;
        }

        /// <summary>
        /// Distance around the polygon.
        /// </summary>
        public override double Length
        {
            get { return Perimeter(); }
        }

        public override Entity Clone()
        {
            var copy = new Polygon { Vertices = new List<Vector>(Vertices) };
            CopyBaseTo(copy);
            return copy;
        }

        /// <summary>
        /// Reverses the rotation direction of the polygon.
        /// </summary>
        public override void Reverse()
        {
            Vertices.Reverse();
        }

        /// <summary>
        /// Moves the start point to the given coordinates.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        public override void MoveTo(double x, double y)
        {
            if (Vertices.Count == 0)
                return;

            var first = Vertices[0];
            var offset = new Vector(x - first.X, y - first.Y);

            Vertices.ForEach(vertex => vertex += offset);
            boundingBox.Offset(offset);
        }

        /// <summary>
        /// Moves the start point to the given point.
        /// </summary>
        /// <param name="pt"></param>
        public override void MoveTo(Vector pt)
        {
            if (Vertices.Count == 0)
                return;

            var first = Vertices[0];
            var offset = pt - first;

            Vertices.ForEach(vertex => vertex += offset);
            boundingBox.Offset(offset);
        }

        /// <summary>
        /// Offsets the location by the given distances.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        public override void Offset(double x, double y)
        {
            for (int i = 0; i < Vertices.Count; i++)
                Vertices[i] = Vertices[i].Offset(x, y);

            boundingBox.Offset(x, y);
        }

        /// <summary>
        /// Offsets the location by the given distances.
        /// </summary>
        /// <param name="voffset"></param>
        public override void Offset(Vector voffset)
        {
            for (int i = 0; i < Vertices.Count; i++)
                Vertices[i] = Vertices[i].Offset(voffset);

            boundingBox.Offset(voffset);
        }

        /// <summary>
        /// Scales the polygon from the zero point.
        /// </summary>
        /// <param name="factor"></param>
        public override void Scale(double factor)
        {
            for (int i = 0; i < Vertices.Count; i++)
                Vertices[i] *= factor;

            UpdateBounds();
        }

        /// <summary>
        /// Scales the polygon from the zero point.
        /// </summary>
        /// <param name="factor"></param>
        /// <param name="origin"></param>
        public override void Scale(double factor, Vector origin)
        {
            for (int i = 0; i < Vertices.Count; i++)
                Vertices[i] = (Vertices[i] - origin) * factor + origin;

            UpdateBounds();
        }

        /// <summary>
        /// Rotates the polygon from the zero point.
        /// </summary>
        /// <param name="angle"></param>
        public override void Rotate(double angle)
        {
            for (int i = 0; i < Vertices.Count; i++)
                Vertices[i] = Vertices[i].Rotate(angle);

            UpdateBounds();
        }

        /// <summary>
        /// Rotates the polygon from the origin.
        /// </summary>
        /// <param name="angle"></param>
        /// <param name="origin"></param>
        public override void Rotate(double angle, Vector origin)
        {
            for (int i = 0; i < Vertices.Count; i++)
                Vertices[i] = Vertices[i].Rotate(angle, origin);

            UpdateBounds();
        }

        /// <summary>
        /// Updates the bounding box.
        /// </summary>
        public override void UpdateBounds()
        {
            if (Vertices.Count == 0)
                return;

            var first = Vertices[0];
            var minX = first.X;
            var maxX = first.X;
            var minY = first.Y;
            var maxY = first.Y;

            for (int i = 1; i < Vertices.Count; ++i)
            {
                var vertex = Vertices[i];

                if (vertex.X < minX) minX = vertex.X;
                else if (vertex.X > maxX) maxX = vertex.X;

                if (vertex.Y < minY) minY = vertex.Y;
                else if (vertex.Y > maxY) maxY = vertex.Y;
            }

            boundingBox.X = minX;
            boundingBox.Y = minY;
            boundingBox.Length = maxX - minX;
            boundingBox.Width = maxY - minY;
        }

        public override Entity OffsetEntity(double distance, OffsetSide side)
        {
            if (Vertices.Count < 3)
                return null;

            var isClosed = IsClosed();
            var count = isClosed ? Vertices.Count - 1 : Vertices.Count;
            if (count < 3)
                return null;

            var ccw = CalculateArea() > 0;
            var outward = ccw ? OffsetSide.Left : OffsetSide.Right;
            var sign = side == outward ? 1.0 : -1.0;
            var d = distance * sign;

            var normals = new Vector[count];
            for (var i = 0; i < count; i++)
            {
                var next = (i + 1) % count;
                var dx = Vertices[next].X - Vertices[i].X;
                var dy = Vertices[next].Y - Vertices[i].Y;
                var len = System.Math.Sqrt(dx * dx + dy * dy);
                if (len < Tolerance.Epsilon)
                    return null;
                normals[i] = new Vector(-dy / len * d, dx / len * d);
            }

            var result = new Polygon();
            for (var i = 0; i < count; i++)
            {
                var prev = (i - 1 + count) % count;

                var a1 = new Vector(Vertices[prev].X + normals[prev].X, Vertices[prev].Y + normals[prev].Y);
                var a2 = new Vector(Vertices[i].X + normals[prev].X, Vertices[i].Y + normals[prev].Y);
                var b1 = new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y);
                var b2 = new Vector(Vertices[(i + 1) % count].X + normals[i].X, Vertices[(i + 1) % count].Y + normals[i].Y);

                var edgeA = new Line(a1, a2);
                var edgeB = new Line(b1, b2);

                if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
                    result.Vertices.Add(pt);
                else
                    result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y));
            }

            result.Close();
            result.RemoveSelfIntersections();
            result.UpdateBounds();
            return result;
        }

        public override Entity OffsetEntity(double distance, Vector pt)
        {
            var left = OffsetEntity(distance, OffsetSide.Left);
            var right = OffsetEntity(distance, OffsetSide.Right);

            if (left == null) return right;
            if (right == null) return left;

            var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
            var distRight = right.ClosestPointTo(pt).DistanceTo(pt);

            return distLeft > distRight ? left : right;
        }

        /// <summary>
        /// Gets the closest point on the polygon to the given point.
        /// </summary>
        /// <param name="pt"></param>
        /// <returns></returns>
        public override Vector ClosestPointTo(Vector pt)
        {
            var lines = ToLines();

            if (lines.Count == 0)
                return Vector.Invalid;

            Vector closestPt = lines[0].ClosestPointTo(pt);
            double distance = closestPt.DistanceTo(pt);

            for (int i = 1; i < lines.Count; i++)
            {
                var line = lines[i];
                var closestPt2 = line.ClosestPointTo(pt);
                var distance2 = closestPt2.DistanceTo(pt);

                if (distance2 < distance)
                {
                    closestPt = closestPt2;
                    distance = distance2;
                }
            }

            return closestPt;
        }

        /// <summary>
        /// Returns true if the given arc is intersecting this.
        /// </summary>
        /// <param name="arc"></param>
        /// <returns></returns>
        public override bool Intersects(Arc arc)
        {
            List<Vector> pts;
            return Intersect.Intersects(arc, this, out pts);
        }

        /// <summary>
        /// Returns true if the given arc is intersecting this.
        /// </summary>
        /// <param name="arc"></param>
        /// <param name="pts"></param>
        /// <returns></returns>
        public override bool Intersects(Arc arc, out List<Vector> pts)
        {
            return Intersect.Intersects(arc, this, out pts);
        }

        /// <summary>
        /// Returns true if the given circle is intersecting this.
        /// </summary>
        /// <param name="circle"></param>
        /// <returns></returns>
        public override bool Intersects(Circle circle)
        {
            List<Vector> pts;
            return Intersect.Intersects(circle, this, out pts);
        }

        /// <summary>
        /// Returns true if the given circle is intersecting this.
        /// </summary>
        /// <param name="circle"></param>
        /// <param name="pts"></param>
        /// <returns></returns>
        public override bool Intersects(Circle circle, out List<Vector> pts)
        {
            return Intersect.Intersects(circle, this, out pts);
        }

        /// <summary>
        /// Returns true if the given line is intersecting this.
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        public override bool Intersects(Line line)
        {
            List<Vector> pts;
            return Intersect.Intersects(line, this, out pts);
        }

        /// <summary>
        /// Returns true if the given line is intersecting this.
        /// </summary>
        /// <param name="line"></param>
        /// <param name="pts"></param>
        /// <returns></returns>
        public override bool Intersects(Line line, out List<Vector> pts)
        {
            return Intersect.Intersects(line, this, out pts);
        }

        /// <summary>
        /// Returns true if the given polygon is intersecting this.
        /// </summary>
        /// <param name="polygon"></param>
        /// <returns></returns>
        public override bool Intersects(Polygon polygon)
        {
            List<Vector> pts;
            return Intersect.Intersects(this, polygon, out pts);
        }

        /// <summary>
        /// Returns true if the given polygon is intersecting this.
        /// </summary>
        /// <param name="polygon"></param>
        /// <param name="pts"></param>
        /// <returns></returns>
        public override bool Intersects(Polygon polygon, out List<Vector> pts)
        {
            return Intersect.Intersects(this, polygon, out pts);
        }

        /// <summary>
        /// Returns true if the given shape is intersecting this.
        /// </summary>
        /// <param name="shape"></param>
        /// <returns></returns>
        public override bool Intersects(Shape shape)
        {
            List<Vector> pts;
            return Intersect.Intersects(shape, this, out pts);
        }

        /// <summary>
        /// Returns true if the given shape is intersecting this.
        /// </summary>
        /// <param name="shape"></param>
        /// <param name="pts"></param>
        /// <returns></returns>
        public override bool Intersects(Shape shape, out List<Vector> pts)
        {
            return Intersect.Intersects(shape, this, out pts);
        }

        /// <summary>
        /// Type of entity.
        /// </summary>
        public override EntityType Type
        {
            get { return EntityType.Polygon; }
        }

        /// <summary>
        /// Removes self-intersecting loops from the polygon by finding non-adjacent
        /// edge crossings and keeping the larger contour at each crossing.
        /// </summary>
        public void RemoveSelfIntersections()
        {
            if (!IsClosed() || Vertices.Count < 5)
                return;

            while (FindCrossing(out var edgeI, out var edgeJ, out var pt))
            {
                Vertices = SplitAtCrossing(edgeI, edgeJ, pt);
            }
        }

        private bool FindCrossing(out int edgeI, out int edgeJ, out Vector pt)
        {
            var n = Vertices.Count - 1;

            // Pre-calculate edge bounding boxes to speed up intersection checks.
            var edgeBounds = new (double minX, double maxX, double minY, double maxY)[n];
            for (var i = 0; i < n; i++)
            {
                var v1 = Vertices[i];
                var v2 = Vertices[i + 1];
                edgeBounds[i] = (
                    System.Math.Min(v1.X, v2.X) - Tolerance.Epsilon,
                    System.Math.Max(v1.X, v2.X) + Tolerance.Epsilon,
                    System.Math.Min(v1.Y, v2.Y) - Tolerance.Epsilon,
                    System.Math.Max(v1.Y, v2.Y) + Tolerance.Epsilon
                );
            }

            for (var i = 0; i < n; i++)
            {
                var bi = edgeBounds[i];
                for (var j = i + 2; j < n; j++)
                {
                    if (i == 0 && j == n - 1)
                        continue;

                    var bj = edgeBounds[j];

                    // Prune with bounding box check.
                    if (bi.maxX < bj.minX || bj.maxX < bi.minX ||
                        bi.maxY < bj.minY || bj.maxY < bi.minY)
                    {
                        continue;
                    }

                    if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt))
                    {
                        edgeI = i;
                        edgeJ = j;
                        return true;
                    }
                }
            }

            edgeI = edgeJ = -1;
            pt = Vector.Zero;
            return false;
        }

        private List<Vector> SplitAtCrossing(int edgeI, int edgeJ, Vector pt)
        {
            var n = Vertices.Count - 1;

            var loopA = Vertices.GetRange(0, edgeI + 1);
            loopA.Add(pt);
            loopA.AddRange(Vertices.GetRange(edgeJ + 1, n - edgeJ - 1));
            loopA.Add(loopA[0]);

            var loopB = new List<Vector> { pt };
            loopB.AddRange(Vertices.GetRange(edgeI + 1, edgeJ - edgeI));
            loopB.Add(pt);

            var areaA = System.Math.Abs(CalculateArea(loopA));
            var areaB = System.Math.Abs(CalculateArea(loopB));

            return areaA >= areaB ? loopA : loopB;
        }

        private static bool SegmentsIntersect(Vector a1, Vector a2, Vector b1, Vector b2, out Vector pt)
        {
            var da = a2 - a1;
            var db = b2 - b1;
            var cross = da.X * db.Y - da.Y * db.X;

            if (cross.IsEqualTo(0.0))
            {
                pt = Vector.Zero;
                return false;
            }

            var dc = b1 - a1;
            var t = (dc.X * db.Y - dc.Y * db.X) / cross;
            var u = (dc.X * da.Y - dc.Y * da.X) / cross;

            if (t > Tolerance.Epsilon && t < 1.0 - Tolerance.Epsilon &&
                u > Tolerance.Epsilon && u < 1.0 - Tolerance.Epsilon)
            {
                pt = new Vector(a1.X + t * da.X, a1.Y + t * da.Y);
                return true;
            }

            pt = Vector.Zero;
            return false;
        }

        private static double CalculateArea(List<Vector> vertices)
        {
            double xsum = 0;
            double ysum = 0;

            for (int i = 0; i < vertices.Count - 1; i++)
            {
                var current = vertices[i];
                var next = vertices[i + 1];

                xsum += current.X * next.Y;
                ysum += current.Y * next.X;
            }

            return (xsum - ysum) * 0.5;
        }

        internal void Cleanup()
        {
            for (int i = Vertices.Count - 1; i > 0; i--)
            {
                var vertex = Vertices[i];
                var nextVertex = Vertices[i - 1];

                if (vertex == nextVertex)
                    Vertices.RemoveAt(i);
            }
        }

        public BoundingRectangleResult FindBestRotation()
        {
            return RotatingCalipers.MinimumBoundingRectangle(Vertices);
        }

        public BoundingRectangleResult FindBestRotation(double startAngle, double endAngle)
        {
            var hull = ConvexHull.Compute(Vertices);
            return RotatingCalipers.MinimumBoundingRectangle(hull, startAngle, endAngle);
        }

        public bool ContainsPoint(Vector pt)
        {
            var n = IsClosed() ? Vertices.Count - 1 : Vertices.Count;

            if (n < 3)
                return false;

            var inside = false;

            for (int i = 0, j = n - 1; i < n; j = i++)
            {
                var vi = Vertices[i];
                var vj = Vertices[j];

                if ((vi.Y > pt.Y) != (vj.Y > pt.Y) &&
                    pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X)
                {
                    inside = !inside;
                }
            }

            return inside;
        }
    }
}
