using System;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine.Rendering;

namespace UnityEngine.Splines
{
    /// <summary>
    /// Utility methods for creating and working with meshes.
    /// </summary>
    public static class SplineMesh
    {
        const float k_RadiusMin = .00001f, k_RadiusMax = 10000f;
        const int k_SidesMin = 3, k_SidesMax = 2084;
        const int k_SegmentsMin = 2, k_SegmentsMax = 4096;

        static readonly VertexAttributeDescriptor[] k_PipeVertexAttribs = new VertexAttributeDescriptor[]
        {
            new (VertexAttribute.Position),
            new (VertexAttribute.Normal),
            new (VertexAttribute.TexCoord0, dimension: 2)
        };

        /// <summary>
        /// Interface for Spline mesh vertex data. Implement this interface if you are extruding custom mesh data and
        /// do not want to use the vertex layout provided by <see cref="SplineMesh"/>."/>.
        /// </summary>
        public interface ISplineVertexData
        {
            /// <summary>
            /// Vertex position.
            /// </summary>
            public Vector3 position { get; set; }

            /// <summary>
            /// Vertex normal.
            /// </summary>
            public Vector3 normal { get; set; }

            /// <summary>
            /// Vertex texture, corresponds to UV0.
            /// </summary>
            public Vector2 texture { get; set; }
        }

        [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
        struct VertexData : ISplineVertexData
        {
            public Vector3 position { get; set; }
            public Vector3 normal { get; set; }
            public Vector2 texture { get; set; }
        }

        static void ExtrudeRing<T, K>(T spline, float t, NativeArray<K> data, int start, int count, float radius)
            where T : ISpline
            where K : struct, ISplineVertexData
        {
            var evaluationT = spline.Closed ? math.frac(t) : math.clamp(t, 0f, 1f);
            spline.Evaluate(evaluationT, out var sp, out var st, out _);
            
            var length = math.lengthsq(st);
            if (length == 0f || float.IsNaN(length))
                st = spline.EvaluateTangent(math.clamp(evaluationT + (0.0001f * (t < 1f ? 1f : -1f)), 0f, 1f));
            st = math.normalize(st);

            var rot = Quaternion.LookRotation(st);
            var rad = math.radians(360f / count);

            for (int n = 0; n < count; ++n)
            {
                var vertex = new K();
                var p = new Vector3(math.cos(n * rad), math.sin(n * rad), 0f) * radius;
                vertex.position = (Vector3)sp + (rot * p);
                vertex.normal = (vertex.position - (Vector3)sp).normalized;

                // instead of inserting a seam, wrap UVs using a triangle wave so that texture wraps back onto itself
                float ut = n / ((float)count + count%2);
                float u = math.abs(ut - math.floor(ut + .5f)) * 2f;
                vertex.texture = new Vector2(u, t * spline.GetLength());

                data[start + n] = vertex;
            }
        }

        // The logic around when caps and closing is a little complicated and easy to confuse. This wraps settings in a
        // consistent way so that methods aren't working with mixed data.
        struct Settings
        {
            public int sides { get; private set; }
            public int segments { get; private set; }
            public bool capped { get; private set; }
            public bool closed { get; private set; }
            public float2 range { get; private set; }
            public float radius { get; private set; }

            public Settings(int sides, int segments, bool capped, bool closed, float2 range, float radius)
            {
                this.sides = math.clamp(sides, k_SidesMin, k_SidesMax);
                this.segments = math.clamp(segments, k_SegmentsMin, k_SegmentsMax);
                this.range = new float2(math.min(range.x, range.y), math.max(range.x, range.y));
                this.closed = math.abs(1f - (this.range.y - this.range.x)) < float.Epsilon && closed;
                this.capped = capped && !this.closed;
                this.radius = math.clamp(radius, k_RadiusMin, k_RadiusMax);
            }
        }

        /// <summary>
        /// Calculate the vertex and index count required for an extruded mesh.
        /// Use this method to allocate attribute and index buffers for use with Extrude.
        /// </summary>
        /// <param name="vertexCount">The number of vertices required for an extruded mesh using the provided settings.</param>
        /// <param name="indexCount">The number of indices required for an extruded mesh using the provided settings.</param>
        /// <param name="sides">How many sides make up the radius of the mesh.</param>
        /// <param name="segments">How many sections compose the length of the mesh.</param>
        /// <param name="range">
        /// The section of the Spline to extrude. This value expects a normalized interpolation start and end.
        /// I.e., [0,1] is the entire Spline, whereas [.5, 1] is the last half of the Spline.
        /// </param>
        /// <param name="capped">Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.</param>
        /// <param name="closed">Whether the extruded mesh is closed or open. This can be separate from the Spline.Closed value.</param>
        public static void GetVertexAndIndexCount(int sides, int segments, bool capped, bool closed, Vector2 range, out int vertexCount, out int indexCount)
        {
            var settings = new Settings(sides, segments, capped, closed, range, 1f);
            GetVertexAndIndexCount(settings, out vertexCount, out indexCount);
        }

        static void GetVertexAndIndexCount(Settings settings, out int vertexCount, out int indexCount)
        {
            vertexCount = settings.sides * (settings.segments + (settings.capped ? 2 : 0));
            indexCount = settings.sides * 6 * (settings.segments - (settings.closed ? 0 : 1)) + (settings.capped ? (settings.sides - 2) * 3 * 2 : 0);
        }

        /// <summary>
        /// Extrude a mesh along a spline in a tube-like shape.
        /// </summary>
        /// <param name="spline">The spline to extrude.</param>
        /// <param name="mesh">A mesh that will be cleared and filled with vertex data for the shape.</param>
        /// <param name="radius">The radius of the extruded mesh.</param>
        /// <param name="sides">How many sides make up the radius of the mesh.</param>
        /// <param name="segments">How many sections compose the length of the mesh.</param>
        /// <param name="capped">Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.</param>
        /// <typeparam name="T">A type implementing ISpline.</typeparam>
        public static void Extrude<T>(T spline, Mesh mesh, float radius, int sides, int segments, bool capped = true) where T : ISpline
        {
            Extrude(spline, mesh, radius, sides, segments, capped, new float2(0f, 1f));
        }

        /// <summary>
        /// Extrude a mesh along a spline in a tube-like shape.
        /// </summary>
        /// <param name="spline">The spline to extrude.</param>
        /// <param name="mesh">A mesh that will be cleared and filled with vertex data for the shape.</param>
        /// <param name="radius">The radius of the extruded mesh.</param>
        /// <param name="sides">How many sides make up the radius of the mesh.</param>
        /// <param name="segments">How many sections compose the length of the mesh.</param>
        /// <param name="capped">Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.</param>
        /// <param name="range">
        /// The section of the Spline to extrude. This value expects a normalized interpolation start and end.
        /// I.e., [0,1] is the entire Spline, whereas [.5, 1] is the last half of the Spline.
        /// </param>
        /// <typeparam name="T">A type implementing ISpline.</typeparam>
        public static void Extrude<T>(T spline, Mesh mesh, float radius, int sides, int segments, bool capped, float2 range) where T : ISpline
        {
            var settings = new Settings(sides, segments, capped, spline.Closed, range, radius);
            GetVertexAndIndexCount(settings, out var vertexCount, out var indexCount);

            var meshDataArray = Mesh.AllocateWritableMeshData(1);
            var data = meshDataArray[0];

            var indexFormat = vertexCount >= ushort.MaxValue ? IndexFormat.UInt32 : IndexFormat.UInt16;
            data.SetIndexBufferParams(indexCount, indexFormat);
            data.SetVertexBufferParams(vertexCount, k_PipeVertexAttribs);

            var vertices = data.GetVertexData<VertexData>();

            if (indexFormat == IndexFormat.UInt16)
            {
                var indices = data.GetIndexData<UInt16>();
                Extrude(spline, vertices, indices, radius, sides, segments, capped, range);
            }
            else
            {
                var indices = data.GetIndexData<UInt32>();
                Extrude(spline, vertices, indices, radius, sides, segments, capped, range);
            }

            mesh.Clear();
            data.subMeshCount = 1;
            data.SetSubMesh(0, new SubMeshDescriptor(0, indexCount));
            Mesh.ApplyAndDisposeWritableMeshData(meshDataArray, mesh);
            mesh.RecalculateBounds();
        }

        /// <summary>
        /// Extrude a mesh along a spline in a tube-like shape.
        /// </summary>
        /// <param name="spline">The spline to extrude.</param>
        /// <param name="vertices">A pre-allocated buffer of vertex data.</param>
        /// <param name="indices">A pre-allocated index buffer. Must be of type UInt16 or UInt32.</param>
        /// <param name="radius">The radius of the extruded mesh.</param>
        /// <param name="sides">How many sides make up the radius of the mesh.</param>
        /// <param name="segments">How many sections compose the length of the mesh.</param>
        /// <param name="capped">Whether the start and end of the mesh is filled. This setting is ignored when spline
        /// is closed.</param>
        /// <param name="range">
        /// The section of the Spline to extrude. This value expects a normalized interpolation start and end.
        /// I.e., [0,1] is the entire Spline, whereas [.5, 1] is the last half of the Spline.
        /// </param>
        /// <typeparam name="TSplineType">A type implementing ISpline.</typeparam>
        /// <typeparam name="TVertexType">A type implementing ISplineVertexData.</typeparam>
        /// <typeparam name="TIndexType">The mesh index format. Must be UInt16 or UInt32.</typeparam>
        /// <exception cref="ArgumentOutOfRangeException">An out of range exception is thrown if the vertex or index
        /// buffer lengths do not match the expected size. Use <see cref="GetVertexAndIndexCount"/> to calculate the
        /// expected buffer sizes.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// An argument exception is thrown if {TIndexType} is not UInt16 or UInt32.
        /// </exception>
        public static void Extrude<TSplineType, TVertexType, TIndexType>(
            TSplineType spline,
            NativeArray<TVertexType> vertices,
            NativeArray<TIndexType> indices,
            float radius,
            int sides,
            int segments,
            bool capped,
            float2 range)
            where TSplineType : ISpline
            where TVertexType : struct, ISplineVertexData
            where TIndexType : struct
        {
            Extrude(spline, vertices, indices, new Settings(sides, segments, capped, spline.Closed, range, radius));
        }

        static void Extrude<TSplineType, TVertexType, TIndexType>(
                TSplineType spline,
                NativeArray<TVertexType> vertices,
                NativeArray<TIndexType> indices,
                Settings settings)
                where TSplineType : ISpline
                where TVertexType : struct, ISplineVertexData
                where TIndexType : struct
        {
            var radius = settings.radius;
            var sides = settings.sides;
            var segments = settings.segments;
            var range = settings.range;
            var capped = settings.capped;
            var closed = settings.closed;

            GetVertexAndIndexCount(settings, out var vertexCount, out var indexCount);

            if (sides < 3)
                throw new ArgumentOutOfRangeException(nameof(sides), "Sides must be greater than 3");

            if (segments < 2)
                throw new ArgumentOutOfRangeException(nameof(segments), "Segments must be greater than 2");

            if (vertices.Length != vertexCount)
                throw new ArgumentOutOfRangeException($"Vertex array is incorrect size. Expected {vertexCount}, but received {vertices.Length}.");

            if (indices.Length != indexCount)
                throw new ArgumentOutOfRangeException($"Index array is incorrect size. Expected {indexCount}, but received {indices.Length}.");

            if (typeof(TIndexType) == typeof(UInt16))
            {
                var ushortIndices = indices.Reinterpret<UInt16>();
                WindTris(ushortIndices, settings);
            }
            else if (typeof(TIndexType) == typeof(UInt32))
            {
                var ulongIndices = indices.Reinterpret<UInt32>();
                WindTris(ulongIndices, settings);
            }
            else
            {
                throw new ArgumentException("Indices must be UInt16 or UInt32", nameof(indices));
            }

            for (int i = 0; i < segments; ++i)
                ExtrudeRing(spline, math.lerp(range.x, range.y, i / (segments - 1f)), vertices, i * sides, sides, radius);

            if (capped)
            {
                var capVertexStart = segments * sides;
                var endCapVertexStart = (segments + 1) * sides;

                var rng = spline.Closed ? math.frac(range) : math.clamp(range, 0f, 1f);
                ExtrudeRing(spline, rng.x, vertices, capVertexStart, sides, radius);
                ExtrudeRing(spline, rng.y, vertices, endCapVertexStart, sides, radius);

                var beginAccel = math.normalize(spline.EvaluateTangent(rng.x));
                var accelLen = math.lengthsq(beginAccel);
                if (accelLen == 0f || float.IsNaN(accelLen))
                    beginAccel = math.normalize(spline.EvaluateTangent(rng.x + 0.0001f));
                var endAccel = math.normalize(spline.EvaluateTangent(rng.y));
                accelLen = math.lengthsq(endAccel);
                if (accelLen == 0f || float.IsNaN(accelLen))
                    endAccel = math.normalize(spline.EvaluateTangent(rng.y - 0.0001f));

                var rad = math.radians(360f / sides);
                var off = new float2(.5f, .5f);

                for (int i = 0; i < sides; ++i)
                {
                    var v0 = vertices[capVertexStart + i];
                    var v1 = vertices[endCapVertexStart + i];

                    v0.normal = -beginAccel;
                    v0.texture = off + new float2(math.cos(i * rad), math.sin(i * rad)) * .5f;

                    v1.normal = endAccel;
                    v1.texture = off + new float2(-math.cos(i * rad), math.sin(i * rad)) * .5f;

                    vertices[capVertexStart + i] = v0;
                    vertices[endCapVertexStart + i] = v1;
                }
            }
        }

        // Two overloads for winding triangles because there is no generic constraint for UInt{16, 32}
        static void WindTris(NativeArray<UInt16> indices, Settings settings)
        {
            var closed = settings.closed;
            var segments = settings.segments;
            var sides = settings.sides;
            var capped = settings.capped;

            for (int i = 0; i < (closed ? segments : segments - 1); ++i)
            {
                for (int n = 0; n < sides; ++n)
                {
                    var index0 = i * sides + n;
                    var index1 = i * sides + ((n + 1) % sides);
                    var index2 = ((i+1) % segments) * sides + n;
                    var index3 = ((i+1) % segments) * sides + ((n + 1) % sides);

                    indices[i * sides * 6 + n * 6 + 0] = (UInt16) index0;
                    indices[i * sides * 6 + n * 6 + 1] = (UInt16) index1;
                    indices[i * sides * 6 + n * 6 + 2] = (UInt16) index2;
                    indices[i * sides * 6 + n * 6 + 3] = (UInt16) index1;
                    indices[i * sides * 6 + n * 6 + 4] = (UInt16) index3;
                    indices[i * sides * 6 + n * 6 + 5] = (UInt16) index2;
                }
            }

            if (capped)
            {
                var capVertexStart = segments * sides;
                var capIndexStart = sides * 6 * (segments-1);
                var endCapVertexStart = (segments + 1) * sides;
                var endCapIndexStart = (segments-1) * 6 * sides + (sides-2) * 3;

                for(ushort i = 0; i < sides - 2; ++i)
                {
                    indices[capIndexStart + i * 3 + 0] = (UInt16)(capVertexStart);
                    indices[capIndexStart + i * 3 + 1] = (UInt16)(capVertexStart + i + 2);
                    indices[capIndexStart + i * 3 + 2] = (UInt16)(capVertexStart + i + 1);

                    indices[endCapIndexStart + i * 3 + 0] = (UInt16) (endCapVertexStart);
                    indices[endCapIndexStart + i * 3 + 1] = (UInt16) (endCapVertexStart + i + 1);
                    indices[endCapIndexStart + i * 3 + 2] = (UInt16) (endCapVertexStart + i + 2);
                }
            }
        }

        // Two overloads for winding triangles because there is no generic constraint for UInt{16, 32}
        static void WindTris(NativeArray<UInt32> indices, Settings settings)
        {
            var closed = settings.closed;
            var segments = settings.segments;
            var sides = settings.sides;
            var capped = settings.capped;

            for (int i = 0; i < (closed ? segments : segments - 1); ++i)
            {
                for (int n = 0; n < sides; ++n)
                {
                    var index0 = i * sides + n;
                    var index1 = i * sides + ((n + 1) % sides);
                    var index2 = ((i+1) % segments) * sides + n;
                    var index3 = ((i+1) % segments) * sides + ((n + 1) % sides);

                    indices[i * sides * 6 + n * 6 + 0] = (UInt32) index0;
                    indices[i * sides * 6 + n * 6 + 1] = (UInt32) index1;
                    indices[i * sides * 6 + n * 6 + 2] = (UInt32) index2;
                    indices[i * sides * 6 + n * 6 + 3] = (UInt32) index1;
                    indices[i * sides * 6 + n * 6 + 4] = (UInt32) index3;
                    indices[i * sides * 6 + n * 6 + 5] = (UInt32) index2;
                }
            }

            if (capped)
            {
                var capVertexStart = segments * sides;
                var capIndexStart = sides * 6 * (segments-1);
                var endCapVertexStart = (segments + 1) * sides;
                var endCapIndexStart = (segments-1) * 6 * sides + (sides-2) * 3;

                for(ushort i = 0; i < sides - 2; ++i)
                {
                    indices[capIndexStart + i * 3 + 0] = (UInt32)(capVertexStart);
                    indices[capIndexStart + i * 3 + 1] = (UInt32)(capVertexStart + i + 2);
                    indices[capIndexStart + i * 3 + 2] = (UInt32)(capVertexStart + i + 1);

                    indices[endCapIndexStart + i * 3 + 0] = (UInt32) (endCapVertexStart);
                    indices[endCapIndexStart + i * 3 + 1] = (UInt32) (endCapVertexStart + i + 1);
                    indices[endCapIndexStart + i * 3 + 2] = (UInt32) (endCapVertexStart + i + 2);
                }
            }
        }
    }
}