using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics;

namespace Ghost.Graphics.Utilities;

public static unsafe class MeshBuilder
{
    /// <summary>
    /// Creates a unit cube centered at the origin with size 1.
    /// </summary>
    public static void CreateCube(float size, Color128 color, AllocationHandle allocationHandle, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
    {
        var half = size * 0.5f;

        vertices = new UnsafeList<Vertex>(24, allocationHandle);
        indices = new UnsafeList<uint>(36, allocationHandle);

        var corners = new float3[]
        {
            new (-half, -half, -half), new (half, -half, -half),
            new (half, half, -half), new (-half, half, -half),
            new (-half, -half, half), new (half, -half, half),
            new (half, half, half), new (-half, half, half)
        };

        var faces = stackalloc int[]
        {
            0,1,2,3,
            5,4,7,6,
            4,0,3,7,
            1,5,6,2,
            3,2,6,7,
            4,5,1,0
        };

        var uvs = stackalloc float2[] { new(0, 0), new(1, 0), new(1, 1), new(0, 1) };

        for (var f = 0; f < 6; f++)
        {
            var face = &faces[f * 4];
            var baseIndex = (uint)vertices.Count;
            for (var i = 0; i < 4; i++)
            {
                var vertex = new Vertex
                {
                    position = corners[face[i]],
                    normal = float3.zero,
                    tangent = float4.zero,
                    color = color,
                    uv = uvs[i]
                };

                vertices.Add(vertex);
            }

            indices.Add(baseIndex + 0);
            indices.Add(baseIndex + 1);
            indices.Add(baseIndex + 2);
            indices.Add(baseIndex + 0);
            indices.Add(baseIndex + 2);
            indices.Add(baseIndex + 3);
        }

        ComputeNormal(vertices, indices);
        ComputeTangents(vertices, indices);
    }


    /// <summary>
    /// Creates a plane on the XZ axis centered at the origin.
    /// </summary>
    public static void CreatePlane(float width, float depth, Color128 color, AllocationHandle allocationHandle, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
    {
        var hw = width * 0.5f;
        var hd = depth * 0.5f;

        vertices = new UnsafeList<Vertex>(4, allocationHandle);
        indices = new UnsafeList<uint>(6, allocationHandle);

        vertices.Add(new Vertex()
        {
            position = new float3(-hw, 0.0f, -hd),
            normal = float3.zero,
            tangent = float4.zero,
            color = color,
            uv = float2.zero
        });

        vertices.Add(new Vertex()
        {
            position = new float3(hw, 0.0f, -hd),
            normal = float3.zero,
            tangent = float4.zero,
            color = color,
            uv = new float2(1.0f, 0.0f)
        });

        vertices.Add(new Vertex()
        {
            position = new float3(hw, 0.0f, hd),
            normal = float3.zero,
            tangent = float4.zero,
            color = color,
            uv = new float2(1.0f, 1.0f)
        });

        vertices.Add(new Vertex()
        {
            position = new float3(-hw, 0.0f, hd),
            normal = float3.zero,
            tangent = float4.zero,
            color = color,
            uv = new float2(0.0f, 1.0f)
        });

        indices.Add(0);
        indices.Add(1);
        indices.Add(2);
        indices.Add(0);
        indices.Add(2);
        indices.Add(3);

        ComputeNormal(vertices, indices);
        ComputeTangents(vertices, indices);
    }

    /// <summary>
    /// Creates a UV sphere centered at the origin.
    /// </summary>
    public static void CreateSphere(int latitudeSegments, int longitudeSegments, float radius, Color128 color, AllocationHandle allocationHandle, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
    {
        vertices = new UnsafeList<Vertex>((latitudeSegments + 1) * (longitudeSegments + 1), allocationHandle);
        indices = new UnsafeList<uint>(latitudeSegments * longitudeSegments * 6, allocationHandle);

        // Vertices
        for (var lat = 0; lat <= latitudeSegments; lat++)
        {
            var theta = (float)lat / latitudeSegments * MathF.PI;
            var sinTheta = MathF.Sin(theta);
            var cosTheta = MathF.Cos(theta);

            for (var lon = 0; lon <= longitudeSegments; lon++)
            {
                var phi = (float)lon / longitudeSegments * 2 * MathF.PI;
                var sinPhi = MathF.Sin(phi);
                var cosPhi = MathF.Cos(phi);

                var x = cosPhi * sinTheta;
                var y = cosTheta;
                var z = sinPhi * sinTheta;

                vertices.Add(new Vertex
                {
                    position = new float3(x, y, z) * radius,
                    normal = float3.zero,
                    tangent = float4.zero,
                    color = color,
                    uv = new float2((float)lon / longitudeSegments, (float)lat / latitudeSegments)
                });
            }
        }

        // Indices
        for (var lat = 0; lat < latitudeSegments; lat++)
        {
            for (var lon = 0; lon < longitudeSegments; lon++)
            {
                var i0 = lat * (longitudeSegments + 1) + lon;
                var i1 = i0 + longitudeSegments + 1;
                var i2 = i1 + 1;
                var i3 = i0 + 1;

                indices.Add((uint)i0);
                indices.Add((uint)i1);
                indices.Add((uint)i2);
                indices.Add((uint)i0);
                indices.Add((uint)i2);
                indices.Add((uint)i3);
            }
        }

        ComputeNormal(vertices, indices);
        ComputeTangents(vertices, indices);
    }

    /// <summary>
    /// Auto-compute smooth per-vertex normals.
    /// </summary>
    /// <param name="vertices">The vertex list.</param>
    /// <param name="indices">The index list.</param>
    public static void ComputeNormal(Span<Vertex> vertices, Span<uint> indices)
    {
        if (vertices.Length < 3 || indices.Length < 3)
        {
            return;
        }

        for (var i = 0; i < indices.Length; i += 3)
        {
            var i0 = (int)indices[i];
            var i1 = (int)indices[i + 1];
            var i2 = (int)indices[i + 2];

            var v0 = vertices[i0];
            var v1 = vertices[i1];
            var v2 = vertices[i2];

            var edge1 = v1.position - v0.position;
            var edge2 = v2.position - v0.position;
            var faceNormal = math.cross(edge1.xyz, edge2.xyz);

            vertices[i0].normal.xyz += faceNormal;
            vertices[i1].normal.xyz += faceNormal;
            vertices[i2].normal.xyz += faceNormal;
        }

        for (var i = 0; i < vertices.Length; i++)
        {
            vertices[i].normal = math.normalize(vertices[i].normal);
        }
    }

    /// <summary>
    /// Auto-compute per-vertex tangents.
    /// </summary>
    /// <param name="vertices">The vertex list.</param>
    /// <param name="indices">The index list.</param>
    public static void ComputeTangents(Span<Vertex> vertices, Span<uint> indices)
    {
        using var scope = AllocationManager.CreateStackScope();
        using var bitangents = new UnsafeArray<float3>(vertices.Length, scope.AllocationHandle, AllocationOption.Clear);

        for (var i = 0; i < indices.Length; i += 3)
        {
            var i0 = (int)indices[i];
            var i1 = (int)indices[i + 1];
            var i2 = (int)indices[i + 2];

            var v0 = vertices[i0];
            var v1 = vertices[i1];
            var v2 = vertices[i2];

            var uv0 = vertices[i0].uv;
            var uv1 = vertices[i1].uv;
            var uv2 = vertices[i2].uv;

            var deltaPos1 = v1.position - v0.position;
            var deltaPos2 = v2.position - v0.position;
            var deltaUV1 = uv1 - uv0;
            var deltaUV2 = uv2 - uv0;

            var r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
            var tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y) * r;
            var bitangent = (deltaPos2 * deltaUV1.x - deltaPos1 * deltaUV2.x) * r;

            for (var j = 0; j < 3; j++)
            {
                var idx = (int)indices[i + j];
                var t = vertices[idx].tangent;
                vertices[idx].tangent.xyz = t.xyz + tangent.xyz;

                bitangents[idx] += bitangent;
            }
        }

        for (var i = 0; i < vertices.Length; i++)
        {
            var n = vertices[i].normal;
            var t = vertices[i].tangent.xyz;

            var proj = n * math.dot(n, t);
            t = math.normalize(t - proj);

            var b = bitangents[i];
            var w = math.dot(math.cross(n.xyz, t.xyz), b.xyz) < 0.0f ? -1.0f : 1.0f;

            vertices[i].tangent = new float4(t.xyz, w);
        }
    }
}
