using System.Collections;
using System.Collections.Generic;
using Syntriax.Engine.Core.Abstract;

namespace Syntriax.Engine.Core;

/// <summary>
/// Represents a shape defined by a collection of vertices.
/// </summary>
/// <param name="vertices">The vertices of the shape.</param>
/// <remarks>
/// Initializes a new instance of the <see cref="Shape2D"/> struct with the specified vertices.
/// </remarks>
[System.Diagnostics.DebuggerDisplay("Vertices Count: {Vertices.Count}")]
public readonly struct Shape2D(List<Vector2D> vertices) : IEnumerable<Vector2D>
{
    public static readonly Shape2D Triangle = CreateNgon(3, Vector2D.Up);
    public static readonly Shape2D Box = CreateNgon(4, Vector2D.One);
    public static readonly Shape2D Pentagon = CreateNgon(5, Vector2D.Up);
    public static readonly Shape2D Hexagon = CreateNgon(6, Vector2D.Right);

    private readonly List<Vector2D> _verticesList = vertices;

    /// <summary>
    /// Gets the vertices of the shape.
    /// </summary>
    public IReadOnlyList<Vector2D> Vertices => _verticesList;

    /// <summary>
    /// The vertex at the specified index.
    /// </summary>
    /// <param name="index">The zero-based index of the vertex to get or set.</param>
    /// <returns>The vertex at the specified index.</returns>
    public Vector2D this[System.Index index] => Vertices[index];

    /// <summary>
    /// Returns a copy of the current shape.
    /// </summary>
    /// <param name="shape">The shape to copy.</param>
    /// <returns>A copy of the input shape.</returns>
    public static Shape2D CreateCopy(Shape2D shape) => new(new List<Vector2D>(shape.Vertices));

    /// <summary>
    /// Creates a regular polygon (ngon) with the specified number of vertices.
    /// </summary>
    /// <param name="vertexCount">The number of vertices in the polygon.</param>
    /// <returns>A regular polygon with the specified number of vertices.</returns>
    public static Shape2D CreateNgon(int vertexCount) => CreateNgon(vertexCount, Vector2D.Up);

    /// <summary>
    /// Creates a regular polygon (ngon) with the specified number of vertices and a rotation position.
    /// </summary>
    /// <param name="vertexCount">The number of vertices in the polygon.</param>
    /// <param name="positionToRotate">The position to use for rotation.</param>
    /// <returns>A regular polygon with the specified number of vertices and rotation position.</returns>
    public static Shape2D CreateNgon(int vertexCount, Vector2D positionToRotate)
    {
        if (vertexCount < 3)
            throw new System.ArgumentException($"{nameof(vertexCount)} must have a value of more than 2.");

        List<Vector2D> vertices = new(vertexCount);

        float radiansPerVertex = 360f / vertexCount * Math.DegreeToRadian;

        for (int i = 0; i < vertexCount; i++)
            vertices.Add(positionToRotate.Rotate(i * radiansPerVertex));

        return new(vertices);
    }

    /// <summary>
    /// Gets the super triangle that encloses the given shape.
    /// </summary>
    /// <param name="shape">The shape to enclose.</param>
    /// <returns>The super triangle that encloses the given shape.</returns>
    public static Triangle GetSuperTriangle(Shape2D shape)
    {
        float minX = float.MaxValue, minY = float.MaxValue;
        float maxX = float.MinValue, maxY = float.MinValue;

        foreach (Vector2D point in shape.Vertices)
        {
            minX = Math.Min(minX, point.X);
            minY = Math.Min(minY, point.Y);
            maxX = Math.Max(maxX, point.X);
            maxY = Math.Max(maxY, point.Y);
        }

        float dx = maxX - minX;
        float dy = maxY - minY;
        float deltaMax = Math.Max(dx, dy);
        float midX = (minX + maxX) / 2;
        float midY = (minY + maxY) / 2;

        Vector2D p1 = new((float)midX - 20f * (float)deltaMax, (float)midY - (float)deltaMax);
        Vector2D p2 = new((float)midX, (float)midY + 20 * (float)deltaMax);
        Vector2D p3 = new((float)midX + 20 * (float)deltaMax, (float)midY - (float)deltaMax);

        return new Triangle(p1, p2, p3);
    }

    /// <summary>
    /// Gets the lines that form the edges of the shape.
    /// </summary>
    /// <param name="shape">The shape to get lines from.</param>
    /// <param name="lines">The list to populate with lines.</param>
    public static void GetLines(Shape2D shape, IList<Line2D> lines)
    {
        lines.Clear();
        for (int i = 0; i < shape.Vertices.Count - 1; i++)
            lines.Add(new(shape.Vertices[i], shape.Vertices[i + 1]));
        lines.Add(new(shape.Vertices[^1], shape.Vertices[0]));
    }

    /// <summary>
    /// Gets a list of lines that form the edges of the shape.
    /// </summary>
    /// <param name="shape">The shape to get lines from.</param>
    /// <returns>A list of lines that form the edges of the shape.</returns>
    public static List<Line2D> GetLines(Shape2D shape)
    {
        List<Line2D> lines = new(shape.Vertices.Count - 1);
        GetLines(shape, lines);
        return lines;
    }

    /// <summary>
    /// Projects the shape onto a vector.
    /// </summary>
    /// <param name="shape">The shape to project.</param>
    /// <param name="projectionVector">The vector to project onto.</param>
    /// <param name="list">The list to populate with projected values.</param>
    public static void Project(Shape2D shape, Vector2D projectionVector, IList<float> list)
    {
        list.Clear();

        int count = shape.Vertices.Count;
        for (int i = 0; i < count; i++)
            list.Add(projectionVector.Dot(shape[i]));
    }

    /// <summary>
    /// Projects the shape onto a vector.
    /// </summary>
    /// <param name="shape">The shape to project.</param>
    /// <param name="projectionVector">The vector to project onto.</param>
    /// <returns>The projection of the shape onto the vector.</returns>
    public static Projection1D Project(Shape2D shape, Vector2D projectionVector)
    {
        float min = float.MaxValue;
        float max = float.MinValue;

        for (int i = 0; i < shape.Vertices.Count; i++)
        {
            float projectedLength = projectionVector.Dot(shape.Vertices[i]);
            min = Math.Min(projectedLength, min);
            max = Math.Max(projectedLength, max);
        }

        return new(min, max);
    }

    /// <summary>
    /// Transforms the shape using the specified transform.
    /// </summary>
    /// <param name="shape">The shape to transform.</param>
    /// <param name="transform">The transform to apply.</param>
    /// <returns>The transformed shape.</returns>
    public static Shape2D TransformShape(Shape2D shape, ITransform transform)
    {
        List<Vector2D> vertices = new(shape.Vertices.Count);

        int count = shape.Vertices.Count;
        for (int i = 0; i < count; i++)
            vertices.Add(transform.TransformVector2D(shape[i]));

        return new Shape2D(vertices);
    }

    /// <summary>
    /// Transforms the shape using the specified transform.
    /// </summary>
    /// <param name="from">The shape to transform.</param>
    /// <param name="transform">The transform to apply.</param>
    /// <param name="to">The transformed shape.</param>
    public static void TransformShape(Shape2D from, ITransform transform, ref Shape2D to)
    {
        to._verticesList.Clear();

        int count = from._verticesList.Count;
        for (int i = 0; i < count; i++)
            to._verticesList.Add(transform.TransformVector2D(from[i]));
    }

    /// <summary>
    /// Determines whether two shapes are approximately equal.
    /// </summary>
    /// <param name="left">The first shape to compare.</param>
    /// <param name="right">The second shape to compare.</param>
    /// <returns><c>true</c> if the shapes are approximately equal; otherwise, <c>false</c>.</returns>
    public static bool ApproximatelyEquals(Shape2D left, Shape2D right, float epsilon = float.Epsilon)
    {
        if (left.Vertices.Count != right.Vertices.Count)
            return false;

        for (int i = 0; i < left.Vertices.Count; i++)
            if (!left.Vertices[i].ApproximatelyEquals(right.Vertices[i], epsilon))
                return false;

        return true;
    }

    /// <inheritdoc/>
    public IEnumerator<Vector2D> GetEnumerator() => Vertices.GetEnumerator();

    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator() => Vertices.GetEnumerator();
}

/// <summary>
/// Provides extension methods for the <see cref="Shape2D"/> struct.
/// </summary>
public static class Shape2DExtensions
{
    /// <summary>
    /// Creates a copy of the shape.
    /// </summary>
    /// <param name="shape">The shape to copy.</param>
    /// <returns>A copy of the input shape.</returns>
    public static Shape2D CreateCopy(this Shape2D shape) => Shape2D.CreateCopy(shape);

    /// <summary>
    /// Gets the super triangle that encloses the shape.
    /// </summary>
    /// <param name="shape">The shape to enclose.</param>
    /// <returns>The super triangle that encloses the shape.</returns>
    public static Triangle ToSuperTriangle(this Shape2D shape) => Shape2D.GetSuperTriangle(shape);

    /// <summary>
    /// Gets the lines that form the edges of the shape.
    /// </summary>
    /// <param name="shape">The shape to get lines from.</param>
    /// <param name="lines">The list to populate with lines.</param>
    public static void ToLines(this Shape2D shape, IList<Line2D> lines) => Shape2D.GetLines(shape, lines);

    /// <summary>
    /// Gets a list of lines that form the edges of the shape.
    /// </summary>
    /// <param name="shape">The shape to get lines from.</param>
    /// <returns>A list of lines that form the edges of the shape.</returns>
    public static List<Line2D> ToLines(this Shape2D shape) => Shape2D.GetLines(shape);

    /// <summary>
    /// Projects the shape onto a vector.
    /// </summary>
    /// <param name="shape">The shape to project.</param>
    /// <param name="projectionVector">The vector to project onto.</param>
    /// <param name="list">The list to populate with projected values.</param>
    public static void ToProjection(this Shape2D shape, Vector2D projectionVector, IList<float> list) => Shape2D.Project(shape, projectionVector, list);

    /// <summary>
    /// Projects the shape onto a vector.
    /// </summary>
    /// <param name="shape">The shape to project.</param>
    /// <param name="projectionVector">The vector to project onto.</param>
    /// <returns>The projection of the shape onto the vector.</returns>
    public static Projection1D ToProjection(this Shape2D shape, Vector2D projectionVector) => Shape2D.Project(shape, projectionVector);

    /// <summary>
    /// Transforms the shape using the specified transform.
    /// </summary>
    /// <param name="transform">The transform to apply.</param>
    /// <param name="shape">The shape to transform.</param>
    /// <returns>The transformed shape.</returns>
    public static Shape2D TransformShape(this ITransform transform, Shape2D shape) => Shape2D.TransformShape(shape, transform);

    /// <summary>
    /// Transforms the shape using the specified transform.
    /// </summary>
    /// <param name="transform">The transform to apply.</param>
    /// <param name="from">The shape to transform.</param>
    /// <param name="to">The transformed shape.</param>
    public static void TransformShape(this ITransform transform, Shape2D from, ref Shape2D to) => Shape2D.TransformShape(from, transform, ref to);

    public static bool ApproximatelyEquals(this Shape2D left, Shape2D right, float epsilon = float.Epsilon) => Shape2D.ApproximatelyEquals(left, right, epsilon);
}