using Lumina.Data.Parsing;
using Lumina.Extensions;
using MeshDecimator;
using MeshDecimator.Algorithms;
using MeshDecimator.Math;
using Microsoft.Extensions.Logging;
using Penumbra.GameData.Files.ModelStructs;
using System.Buffers.Binary;
using MdlFile = Penumbra.GameData.Files.MdlFile;
using MsLogger = Microsoft.Extensions.Logging.ILogger;

namespace LightlessSync.Services.ModelDecimation;

internal static class MdlDecimator
{
    private const int MaxStreams = 3;
    private const int ReadRetryCount = 8;
    private const int ReadRetryDelayMs = 250;

    private static readonly HashSet<MdlFile.VertexUsage> SupportedUsages =
    [
        MdlFile.VertexUsage.Position,
        MdlFile.VertexUsage.Normal,
        MdlFile.VertexUsage.Tangent1,
        MdlFile.VertexUsage.UV,
        MdlFile.VertexUsage.Color,
        MdlFile.VertexUsage.BlendWeights,
        MdlFile.VertexUsage.BlendIndices,
    ];

    private static readonly HashSet<MdlFile.VertexType> SupportedTypes =
    [
        MdlFile.VertexType.Single2,
        MdlFile.VertexType.Single3,
        MdlFile.VertexType.Single4,
        MdlFile.VertexType.Half2,
        MdlFile.VertexType.Half4,
        MdlFile.VertexType.UByte4,
        MdlFile.VertexType.NByte4,
    ];

    public static bool TryDecimate(string sourcePath, string destinationPath, int triangleThreshold, double targetRatio, MsLogger logger)
    {
        try
        {
            if (!TryReadModelBytes(sourcePath, logger, out var data))
            {
                logger.LogInformation("Skipping model decimation; source file locked or unreadable: {Path}", sourcePath);
                return false;
            }
            var mdl = new MdlFile(data);
            if (!mdl.Valid)
            {
                logger.LogInformation("Skipping model decimation; invalid mdl: {Path}", sourcePath);
                return false;
            }

            if (mdl.LodCount != 1)
            {
                logger.LogInformation("Skipping model decimation; unsupported LOD count for {Path}", sourcePath);
                return false;
            }

            if (HasShapeData(mdl))
            {
                logger.LogInformation("Skipping model decimation; shape/morph data present for {Path}", sourcePath);
                return false;
            }

            const int lodIndex = 0;
            var lod = mdl.Lods[lodIndex];
            var meshes = mdl.Meshes.ToArray();
            if (meshes.Length == 0)
            {
                logger.LogInformation("Skipping model decimation; no meshes for {Path}", sourcePath);
                return false;
            }

            if (lod.MeshCount == 0)
            {
                logger.LogInformation("Skipping model decimation; no meshes for {Path}", sourcePath);
                return false;
            }

            var lodMeshStart = (int)lod.MeshIndex;
            var lodMeshEnd = lodMeshStart + lod.MeshCount;
            if (lodMeshStart < 0 || lodMeshEnd > meshes.Length)
            {
                logger.LogInformation("Skipping model decimation; invalid LOD mesh range for {Path}", sourcePath);
                return false;
            }

            var anyDecimated = false;
            var newSubMeshes = new List<MdlStructs.SubmeshStruct>(mdl.SubMeshes.Length);
            var newVertexBuffer = new List<byte>(mdl.VertexBufferSize[lodIndex] > 0 ? (int)mdl.VertexBufferSize[lodIndex] : 0);
            var newIndexBuffer = new List<ushort>(mdl.IndexBufferSize[lodIndex] > 0 ? (int)(mdl.IndexBufferSize[lodIndex] / sizeof(ushort)) : 0);
            var subMeshCursor = 0;
            DecimationAlgorithm? decimationAlgorithm = null;
            int? decimationUvChannelCount = null;

            for (var meshIndex = 0; meshIndex < meshes.Length; meshIndex++)
            {
                var mesh = meshes[meshIndex];
                var meshSubMeshes = mdl.SubMeshes
                    .Skip(mesh.SubMeshIndex)
                    .Take(mesh.SubMeshCount)
                    .ToArray();

                var meshIndexBase = newIndexBuffer.Count;
                var vertexBufferBase = newVertexBuffer.Count;

                MeshStruct updatedMesh;
                MdlStructs.SubmeshStruct[] updatedSubMeshes;
                byte[][] vertexStreams;
                int[] indices;
                bool decimated;

                if (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd
                    && TryProcessMesh(mdl, lodIndex, meshIndex, mesh, meshSubMeshes, triangleThreshold, targetRatio,
                        out updatedMesh,
                        out updatedSubMeshes,
                        out vertexStreams,
                        out indices,
                        out decimated,
                        ref decimationAlgorithm,
                        ref decimationUvChannelCount,
                        logger))
                {
                    updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase);
                }
                else
                {
                    if (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd)
                    {
                        logger.LogDebug("Skipping decimation for mesh {MeshIndex} in {Path}", meshIndex, sourcePath);
                    }

                    updatedMesh = mesh;
                    updatedSubMeshes = CopySubMeshes(meshSubMeshes, meshIndexBase, mesh.StartIndex);
                    vertexStreams = CopyVertexStreams(mdl, lodIndex, mesh);
                    indices = ReadIndices(mdl, lodIndex, mesh);
                    decimated = false;
                }

                anyDecimated |= decimated;

                var vertexCount = updatedMesh.VertexCount;
                var streamSizes = new int[MaxStreams];
                for (var stream = 0; stream < MaxStreams; stream++)
                {
                    var stride = updatedMesh.VertexBufferStride(stream);
                    if (stride > 0 && vertexCount > 0)
                    {
                        streamSizes[stream] = stride * vertexCount;
                    }
                }

                updatedMesh.VertexBufferOffset1 = (uint)vertexBufferBase;
                updatedMesh.VertexBufferOffset2 = (uint)(vertexBufferBase + streamSizes[0]);
                updatedMesh.VertexBufferOffset3 = (uint)(vertexBufferBase + streamSizes[0] + streamSizes[1]);

                newVertexBuffer.AddRange(vertexStreams[0]);
                newVertexBuffer.AddRange(vertexStreams[1]);
                newVertexBuffer.AddRange(vertexStreams[2]);

                updatedMesh.StartIndex = (uint)meshIndexBase;
                updatedMesh.SubMeshIndex = (ushort)subMeshCursor;
                updatedMesh.SubMeshCount = (ushort)updatedSubMeshes.Length;
                updatedMesh.IndexCount = (uint)indices.Length;

                meshes[meshIndex] = updatedMesh;
                newSubMeshes.AddRange(updatedSubMeshes);
                subMeshCursor += updatedSubMeshes.Length;
                newIndexBuffer.AddRange(indices.Select(static i => (ushort)i));
            }

            if (!anyDecimated)
            {
                logger.LogInformation("Skipping model decimation; no eligible meshes for {Path}", sourcePath);
                return false;
            }

            var indexBytes = BuildIndexBytes(newIndexBuffer);

            mdl.Meshes = meshes;
            mdl.SubMeshes = [.. newSubMeshes];
            mdl.VertexOffset[lodIndex] = 0;
            mdl.IndexOffset[lodIndex] = (uint)newVertexBuffer.Count;
            mdl.VertexBufferSize[lodIndex] = (uint)newVertexBuffer.Count;
            mdl.IndexBufferSize[lodIndex] = (uint)indexBytes.Length;

            mdl.Lods[lodIndex] = mdl.Lods[lodIndex] with
            {
                VertexDataOffset = 0,
                VertexBufferSize = (uint)newVertexBuffer.Count,
                IndexDataOffset = (uint)newVertexBuffer.Count,
                IndexBufferSize = (uint)indexBytes.Length,
            };

            for (var clearIndex = 1; clearIndex < mdl.VertexOffset.Length; clearIndex++)
            {
                mdl.VertexOffset[clearIndex] = 0;
                mdl.IndexOffset[clearIndex] = 0;
                mdl.VertexBufferSize[clearIndex] = 0;
                mdl.IndexBufferSize[clearIndex] = 0;

                if (clearIndex < mdl.Lods.Length)
                {
                    mdl.Lods[clearIndex] = mdl.Lods[clearIndex] with
                    {
                        VertexDataOffset = 0,
                        VertexBufferSize = 0,
                        IndexDataOffset = 0,
                        IndexBufferSize = 0,
                    };
                }
            }

            mdl.RemainingData = [.. newVertexBuffer, .. indexBytes];

            var outputData = mdl.Write();
            Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
            var tempPath = destinationPath + ".tmp";
            File.WriteAllBytes(tempPath, outputData);
            File.Move(tempPath, destinationPath, overwrite: true);
            return true;
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to decimate model {Path}", sourcePath);
            return false;
        }
    }

    private static bool TryReadModelBytes(string sourcePath, MsLogger logger, out byte[] data)
    {
        Exception? lastError = null;
        for (var attempt = 0; attempt < ReadRetryCount; attempt++)
        {
            try
            {
                data = ReadAllBytesShared(sourcePath);
                return true;
            }
            catch (IOException ex)
            {
                lastError = ex;
            }
            catch (UnauthorizedAccessException ex)
            {
                lastError = ex;
            }

            if (attempt < ReadRetryCount - 1)
            {
                Thread.Sleep(ReadRetryDelayMs);
            }
        }

        if (lastError != null)
        {
            logger.LogDebug(lastError, "Failed to read model for decimation after {Attempts} attempts: {Path}", ReadRetryCount, sourcePath);
        }

        data = [];
        return false;
    }

    private static byte[] ReadAllBytesShared(string sourcePath)
    {
        using var stream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
        var length = stream.Length;
        if (length <= 0)
        {
            throw new IOException("Model file length is zero.");
        }

        if (length > int.MaxValue)
        {
            throw new IOException("Model file too large.");
        }

        var buffer = new byte[(int)length];
        var totalRead = 0;
        while (totalRead < buffer.Length)
        {
            var read = stream.Read(buffer, totalRead, buffer.Length - totalRead);
            if (read == 0)
            {
                break;
            }

            totalRead += read;
        }

        if (totalRead != buffer.Length || stream.Length != length)
        {
            throw new IOException("Model file length changed during read.");
        }

        return buffer;
    }

    private static bool TryProcessMesh(
        MdlFile mdl,
        int lodIndex,
        int meshIndex,
        MeshStruct mesh,
        MdlStructs.SubmeshStruct[] meshSubMeshes,
        int triangleThreshold,
        double targetRatio,
        out MeshStruct updatedMesh,
        out MdlStructs.SubmeshStruct[] updatedSubMeshes,
        out byte[][] vertexStreams,
        out int[] indices,
        out bool decimated,
        ref DecimationAlgorithm? decimationAlgorithm,
        ref int? decimationUvChannelCount,
        MsLogger logger)
    {
        updatedMesh = mesh;
        updatedSubMeshes = [];
        vertexStreams = [[], [], []];
        indices = [];
        decimated = false;

        if (mesh.VertexCount == 0 || mesh.IndexCount == 0)
        {
            return false;
        }

        if (meshSubMeshes.Length == 0)
        {
            return false;
        }

        var triangleCount = (int)(mesh.IndexCount / 3);
        if (triangleCount < triangleThreshold)
        {
            return false;
        }

        if (!TryBuildVertexFormat(mdl.VertexDeclarations[meshIndex], out var format, out var reason))
        {
            logger.LogDebug("Mesh {MeshIndex} vertex format unsupported: {Reason}", meshIndex, reason);
            return false;
        }

        if (!TryDecodeMeshData(mdl, lodIndex, mesh, format, meshSubMeshes, out var decoded, out var subMeshIndices, out var decodeReason))
        {
            logger.LogDebug("Mesh {MeshIndex} decode failed: {Reason}", meshIndex, decodeReason);
            return false;
        }

        var targetTriangles = (int)Math.Floor(triangleCount * targetRatio);
        if (targetTriangles < 1 || targetTriangles >= triangleCount)
        {
            return false;
        }

        var meshDecimatorMesh = BuildMesh(decoded, subMeshIndices);
        var algorithm = GetOrCreateAlgorithm(format, ref decimationAlgorithm, ref decimationUvChannelCount, logger);
        algorithm.Initialize(meshDecimatorMesh);
        algorithm.DecimateMesh(targetTriangles);
        var decimatedMesh = algorithm.ToMesh();

        if (decimatedMesh.SubMeshCount != meshSubMeshes.Length)
        {
            logger.LogDebug("Mesh {MeshIndex} submesh count changed after decimation", meshIndex);
            return false;
        }

        if (!TryEncodeMeshData(decimatedMesh, format, mesh, meshSubMeshes, out updatedMesh, out updatedSubMeshes, out vertexStreams, out indices, out var encodeReason))
        {
            logger.LogDebug("Mesh {MeshIndex} encode failed: {Reason}", meshIndex, encodeReason);
            return false;
        }

        decimated = true;
        return true;
    }

    private static DecimationAlgorithm GetOrCreateAlgorithm(
        VertexFormat format,
        ref DecimationAlgorithm? decimationAlgorithm,
        ref int? decimationUvChannelCount,
        MsLogger logger)
    {
        var uvChannelCount = format.UvChannelCount;
        if (decimationAlgorithm == null || decimationUvChannelCount != uvChannelCount)
        {
            decimationAlgorithm = MeshDecimation.CreateAlgorithm(Algorithm.Default);
            decimationAlgorithm.Logger = logger;
            decimationUvChannelCount = uvChannelCount;
        }

        return decimationAlgorithm;
    }

    private static Mesh BuildMesh(DecodedMeshData decoded, int[][] subMeshIndices)
    {
        var mesh = new Mesh(decoded.Positions, subMeshIndices);
        if (decoded.Normals != null)
        {
            mesh.Normals = decoded.Normals;
        }

        if (decoded.Tangents != null)
        {
            mesh.Tangents = decoded.Tangents;
        }

        if (decoded.Colors != null)
        {
            mesh.Colors = decoded.Colors;
        }

        if (decoded.BoneWeights != null)
        {
            mesh.BoneWeights = decoded.BoneWeights;
        }

        if (decoded.UvChannels != null)
        {
            for (var channel = 0; channel < decoded.UvChannels.Length; channel++)
            {
                mesh.SetUVs(channel, decoded.UvChannels[channel]);
            }
        }

        return mesh;
    }

    private static bool TryDecodeMeshData(
        MdlFile mdl,
        int lodIndex,
        MeshStruct mesh,
        VertexFormat format,
        MdlStructs.SubmeshStruct[] meshSubMeshes,
        out DecodedMeshData decoded,
        out int[][] subMeshIndices,
        out string? reason)
    {
        decoded = default!;
        subMeshIndices = [];
        reason = null;

        if (!TryBuildSubMeshIndices(mdl, lodIndex, mesh, meshSubMeshes, out subMeshIndices, out reason))
        {
            return false;
        }

        var vertexCount = mesh.VertexCount;
        var positions = new Vector3d[vertexCount];
        Vector3[]? normals = format.HasNormals ? new Vector3[vertexCount] : null;
        Vector4[]? tangents = format.HasTangents ? new Vector4[vertexCount] : null;
        Vector4[]? colors = format.HasColors ? new Vector4[vertexCount] : null;
        BoneWeight[]? boneWeights = format.HasSkinning ? new BoneWeight[vertexCount] : null;

        Vector2[][]? uvChannels = null;
        if (format.UvChannelCount > 0)
        {
            uvChannels = new Vector2[format.UvChannelCount][];
            for (var channel = 0; channel < format.UvChannelCount; channel++)
            {
                uvChannels[channel] = new Vector2[vertexCount];
            }
        }

        var streams = new BinaryReader[MaxStreams];
        for (var streamIndex = 0; streamIndex < MaxStreams; streamIndex++)
        {
            streams[streamIndex] = new BinaryReader(new MemoryStream(mdl.RemainingData));
            streams[streamIndex].BaseStream.Position = mdl.VertexOffset[lodIndex] + mesh.VertexBufferOffset(streamIndex);
        }

        var uvLookup = format.UvElements.ToDictionary(static element => ElementKey.From(element.Element), static element => element);
        for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++)
        {
            byte[]? indices = null;
            float[]? weights = null;

            foreach (var element in format.SortedElements)
            {
                var usage = (MdlFile.VertexUsage)element.Usage;
                var type = (MdlFile.VertexType)element.Type;
                var stream = streams[element.Stream];

                switch (usage)
                {
                    case MdlFile.VertexUsage.Position:
                        positions[vertexIndex] = ReadPosition(type, stream);
                        break;
                    case MdlFile.VertexUsage.Normal when normals != null:
                        normals[vertexIndex] = ReadNormal(type, stream);
                        break;
                    case MdlFile.VertexUsage.Tangent1 when tangents != null:
                        tangents[vertexIndex] = ReadTangent(type, stream);
                        break;
                    case MdlFile.VertexUsage.Color when colors != null:
                        colors[vertexIndex] = ReadColor(type, stream);
                        break;
                    case MdlFile.VertexUsage.BlendIndices:
                        indices = ReadIndices(type, stream);
                        break;
                    case MdlFile.VertexUsage.BlendWeights:
                        weights = ReadWeights(type, stream);
                        break;
                    case MdlFile.VertexUsage.UV when uvChannels != null:
                        if (!uvLookup.TryGetValue(ElementKey.From(element), out var uvElement))
                        {
                            reason = "UV mapping missing.";
                            return false;
                        }
                        ReadUv(type, stream, uvElement, uvChannels, vertexIndex);
                        break;
                    default:
                        if (usage == MdlFile.VertexUsage.Normal || usage == MdlFile.VertexUsage.Tangent1
                            || usage == MdlFile.VertexUsage.Color)
                        {
                            _ = ReadAndDiscard(type, stream);
                        }
                        break;
                }
            }

            if (boneWeights != null)
            {
                if (indices == null || weights == null || indices.Length != 4 || weights.Length != 4)
                {
                    reason = "Missing or invalid skinning data.";
                    return false;
                }

                NormalizeWeights(weights);
                boneWeights[vertexIndex] = new BoneWeight(indices[0], indices[1], indices[2], indices[3], weights[0], weights[1], weights[2], weights[3]);
            }
        }

        decoded = new DecodedMeshData(positions, normals, tangents, colors, boneWeights, uvChannels);
        return true;
    }

    private static bool TryEncodeMeshData(
        Mesh decimatedMesh,
        VertexFormat format,
        MeshStruct originalMesh,
        MdlStructs.SubmeshStruct[] originalSubMeshes,
        out MeshStruct updatedMesh,
        out MdlStructs.SubmeshStruct[] updatedSubMeshes,
        out byte[][] vertexStreams,
        out int[] indices,
        out string? reason)
    {
        updatedMesh = originalMesh;
        updatedSubMeshes = [];
        vertexStreams = [[], [], []];
        indices = [];
        reason = null;

        var vertexCount = decimatedMesh.Vertices.Length;
        if (vertexCount > ushort.MaxValue)
        {
            reason = "Vertex count exceeds ushort range.";
            return false;
        }

        var normals = decimatedMesh.Normals;
        var tangents = decimatedMesh.Tangents;
        var colors = decimatedMesh.Colors;
        var boneWeights = decimatedMesh.BoneWeights;

        if (format.HasNormals && normals == null)
        {
            reason = "Missing normals after decimation.";
            return false;
        }

        if (format.HasTangents && tangents == null)
        {
            reason = "Missing tangents after decimation.";
            return false;
        }

        if (format.HasColors && colors == null)
        {
            reason = "Missing colors after decimation.";
            return false;
        }

        if (format.HasSkinning && boneWeights == null)
        {
            reason = "Missing bone weights after decimation.";
            return false;
        }

        var uvChannels = Array.Empty<Vector2[]>();
        if (format.UvChannelCount > 0)
        {
            uvChannels = new Vector2[format.UvChannelCount][];
            for (var channel = 0; channel < format.UvChannelCount; channel++)
            {
                if (decimatedMesh.GetUVDimension(channel) != 2)
                {
                    reason = "Unsupported UV dimension after decimation.";
                    return false;
                }
                uvChannels[channel] = decimatedMesh.GetUVs2D(channel);
            }
        }

        var streamBuffers = new byte[MaxStreams][];
        for (var stream = 0; stream < MaxStreams; stream++)
        {
            var stride = originalMesh.VertexBufferStride(stream);
            if (stride == 0 || vertexCount == 0)
            {
                streamBuffers[stream] = [];
                continue;
            }

            streamBuffers[stream] = new byte[stride * vertexCount];
        }

        var uvLookup = format.UvElements.ToDictionary(static element => ElementKey.From(element.Element), static element => element);

        foreach (var element in format.SortedElements)
        {
            var stride = originalMesh.VertexBufferStride(element.Stream);
            if (stride == 0)
            {
                continue;
            }

            var elementSize = GetElementSize((MdlFile.VertexType)element.Type);
            if (element.Offset + elementSize > stride)
            {
                reason = "Vertex element stride overflow.";
                return false;
            }
        }

        for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++)
        {
            foreach (var element in format.SortedElements)
            {
                var usage = (MdlFile.VertexUsage)element.Usage;
                var type = (MdlFile.VertexType)element.Type;
                var stream = element.Stream;
                var stride = originalMesh.VertexBufferStride(stream);
                if (stride == 0)
                {
                    continue;
                }

                var baseOffset = vertexIndex * stride + element.Offset;
                var target = streamBuffers[stream].AsSpan(baseOffset, GetElementSize(type));

                switch (usage)
                {
                    case MdlFile.VertexUsage.Position:
                        WritePosition(type, decimatedMesh.Vertices[vertexIndex], target);
                        break;
                    case MdlFile.VertexUsage.Normal when normals != null:
                        WriteNormal(type, normals[vertexIndex], target);
                        break;
                    case MdlFile.VertexUsage.Tangent1 when tangents != null:
                        WriteTangent(type, tangents[vertexIndex], target);
                        break;
                    case MdlFile.VertexUsage.Color when colors != null:
                        WriteColor(type, colors[vertexIndex], target);
                        break;
                    case MdlFile.VertexUsage.BlendIndices when boneWeights != null:
                        WriteBlendIndices(type, boneWeights[vertexIndex], target);
                        break;
                    case MdlFile.VertexUsage.BlendWeights when boneWeights != null:
                        WriteBlendWeights(type, boneWeights[vertexIndex], target);
                        break;
                    case MdlFile.VertexUsage.UV when format.UvChannelCount > 0:
                        if (!uvLookup.TryGetValue(ElementKey.From(element), out var uvElement))
                        {
                            reason = "UV mapping missing.";
                            return false;
                        }
                        WriteUv(type, uvElement, uvChannels, vertexIndex, target);
                        break;
                }
            }
        }

        updatedMesh.VertexCount = (ushort)vertexCount;

        var newSubMeshes = new List<MdlStructs.SubmeshStruct>(originalSubMeshes.Length);
        var indexList = new List<int>();

        for (var subMeshIndex = 0; subMeshIndex < originalSubMeshes.Length; subMeshIndex++)
        {
            var subMeshIndices = decimatedMesh.GetIndices(subMeshIndex);
            if (subMeshIndices.Any(index => index < 0 || index >= vertexCount))
            {
                reason = "Decimated indices out of range.";
                return false;
            }

            var offset = indexList.Count;
            indexList.AddRange(subMeshIndices);

            var updatedSubMesh = originalSubMeshes[subMeshIndex] with
            {
                IndexOffset = (uint)offset,
                IndexCount = (uint)subMeshIndices.Length,
            };
            newSubMeshes.Add(updatedSubMesh);
        }

        updatedSubMeshes = newSubMeshes.ToArray();
        indices = indexList.ToArray();
        vertexStreams = streamBuffers;
        return true;
    }

    private static bool TryBuildSubMeshIndices(
        MdlFile mdl,
        int lodIndex,
        MeshStruct mesh,
        MdlStructs.SubmeshStruct[] meshSubMeshes,
        out int[][] subMeshIndices,
        out string? reason)
    {
        reason = null;
        subMeshIndices = new int[meshSubMeshes.Length][];
        var meshIndices = ReadIndices(mdl, lodIndex, mesh);

        for (var subMeshIndex = 0; subMeshIndex < meshSubMeshes.Length; subMeshIndex++)
        {
            var subMesh = meshSubMeshes[subMeshIndex];
            if (subMesh.IndexCount == 0)
            {
                subMeshIndices[subMeshIndex] = [];
                continue;
            }

            var relativeOffset = (int)(subMesh.IndexOffset - mesh.StartIndex);
            if (relativeOffset < 0 || relativeOffset + subMesh.IndexCount > meshIndices.Length)
            {
                reason = "Submesh index range out of bounds.";
                return false;
            }

            var slice = meshIndices.Skip(relativeOffset).Take((int)subMesh.IndexCount).Select(static i => (int)i).ToArray();
            subMeshIndices[subMeshIndex] = slice;
        }

        return true;
    }

    private static byte[] BuildIndexBytes(List<ushort> indices)
    {
        var indexBytes = new byte[indices.Count * sizeof(ushort)];
        for (var i = 0; i < indices.Count; i++)
        {
            BinaryPrimitives.WriteUInt16LittleEndian(indexBytes.AsSpan(i * 2, 2), indices[i]);
        }

        return indexBytes;
    }

    private static int[] ReadIndices(MdlFile mdl, int lodIndex, MeshStruct mesh)
    {
        using var reader = new BinaryReader(new MemoryStream(mdl.RemainingData));
        reader.BaseStream.Position = mdl.IndexOffset[lodIndex] + mesh.StartIndex * sizeof(ushort);
        var values = reader.ReadStructuresAsArray<ushort>((int)mesh.IndexCount);
        return values.Select(static i => (int)i).ToArray();
    }

    private static byte[][] CopyVertexStreams(MdlFile mdl, int lodIndex, MeshStruct mesh)
    {
        var streams = new byte[MaxStreams][];
        for (var stream = 0; stream < MaxStreams; stream++)
        {
            var stride = mesh.VertexBufferStride(stream);
            if (stride == 0 || mesh.VertexCount == 0)
            {
                streams[stream] = [];
                continue;
            }

            var size = stride * mesh.VertexCount;
            var offset = mdl.VertexOffset[lodIndex] + mesh.VertexBufferOffset(stream);
            streams[stream] = mdl.RemainingData.AsSpan((int)offset, size).ToArray();
        }

        return streams;
    }

    private static MdlStructs.SubmeshStruct[] CopySubMeshes(MdlStructs.SubmeshStruct[] source, int newMeshIndexBase, uint meshStartIndex)
    {
        var result = new MdlStructs.SubmeshStruct[source.Length];
        for (var i = 0; i < source.Length; i++)
        {
            var relativeOffset = (int)(source[i].IndexOffset - meshStartIndex);
            result[i] = source[i] with
            {
                IndexOffset = (uint)(newMeshIndexBase + relativeOffset),
            };
        }

        return result;
    }

    private static MdlStructs.SubmeshStruct[] OffsetSubMeshes(MdlStructs.SubmeshStruct[] source, int meshIndexBase)
    {
        var result = new MdlStructs.SubmeshStruct[source.Length];
        for (var i = 0; i < source.Length; i++)
        {
            result[i] = source[i] with
            {
                IndexOffset = (uint)(meshIndexBase + source[i].IndexOffset),
            };
        }

        return result;
    }

    private static bool TryBuildVertexFormat(MdlStructs.VertexDeclarationStruct declaration, out VertexFormat format, out string? reason)
    {
        reason = null;
        format = default!;

        var elements = declaration.VertexElements;
        foreach (var element in elements)
        {
            if (element.Stream >= MaxStreams)
            {
                reason = "Vertex stream index out of range.";
                return false;
            }

            var usage = (MdlFile.VertexUsage)element.Usage;
            var type = (MdlFile.VertexType)element.Type;

            if (!SupportedUsages.Contains(usage))
            {
                reason = $"Unsupported usage {usage}.";
                return false;
            }

            if (!SupportedTypes.Contains(type))
            {
                reason = $"Unsupported vertex type {type}.";
                return false;
            }
        }

        var positionElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Position).ToArray();
        if (positionElements.Length != 1)
        {
            reason = "Expected single position element.";
            return false;
        }

        var positionType = (MdlFile.VertexType)positionElements[0].Type;
        if (positionType != MdlFile.VertexType.Single3 && positionType != MdlFile.VertexType.Single4)
        {
            reason = "Unsupported position element type.";
            return false;
        }

        var normalElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Normal).ToArray();
        if (normalElements.Length > 1)
        {
            reason = "Multiple normal elements unsupported.";
            return false;
        }

        if (normalElements.Length == 1)
        {
            var normalType = (MdlFile.VertexType)normalElements[0].Type;
            if (normalType != MdlFile.VertexType.Single3 && normalType != MdlFile.VertexType.Single4 && normalType != MdlFile.VertexType.NByte4)
            {
                reason = "Unsupported normal element type.";
                return false;
            }
        }

        var tangentElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Tangent1).ToArray();
        if (tangentElements.Length > 1)
        {
            reason = "Multiple tangent elements unsupported.";
            return false;
        }

        if (tangentElements.Length == 1)
        {
            var tangentType = (MdlFile.VertexType)tangentElements[0].Type;
            if (tangentType != MdlFile.VertexType.Single4 && tangentType != MdlFile.VertexType.NByte4)
            {
                reason = "Unsupported tangent element type.";
                return false;
            }
        }

        var colorElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Color).ToArray();
        if (colorElements.Length > 1)
        {
            reason = "Multiple color elements unsupported.";
            return false;
        }

        MdlStructs.VertexElement? colorElement = null;
        if (colorElements.Length == 1)
        {
            var colorType = (MdlFile.VertexType)colorElements[0].Type;
            if (colorType != MdlFile.VertexType.UByte4 && colorType != MdlFile.VertexType.NByte4 && colorType != MdlFile.VertexType.Single4)
            {
                reason = "Unsupported color element type.";
                return false;
            }

            colorElement = colorElements[0];
        }

        var blendIndicesElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.BlendIndices).ToArray();
        var blendWeightsElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.BlendWeights).ToArray();
        if (blendIndicesElements.Length != blendWeightsElements.Length)
        {
            reason = "Blend indices/weights mismatch.";
            return false;
        }

        if (blendIndicesElements.Length > 1 || blendWeightsElements.Length > 1)
        {
            reason = "Multiple blend elements unsupported.";
            return false;
        }

        if (blendIndicesElements.Length == 1)
        {
            var indexType = (MdlFile.VertexType)blendIndicesElements[0].Type;
            if (indexType != MdlFile.VertexType.UByte4)
            {
                reason = "Unsupported blend index type.";
                return false;
            }

            var weightType = (MdlFile.VertexType)blendWeightsElements[0].Type;
            if (weightType != MdlFile.VertexType.UByte4 && weightType != MdlFile.VertexType.NByte4 && weightType != MdlFile.VertexType.Single4)
            {
                reason = "Unsupported blend weight type.";
                return false;
            }
        }

        if (!TryBuildUvElements(elements, out var uvElements, out var uvChannelCount, out reason))
        {
            return false;
        }

        var sortedElements = elements.OrderBy(static element => element.Offset).ToList();
        format = new VertexFormat(
            sortedElements,
            normalElements.Length == 1 ? normalElements[0] : (MdlStructs.VertexElement?)null,
            tangentElements.Length == 1 ? tangentElements[0] : (MdlStructs.VertexElement?)null,
            colorElement,
            blendIndicesElements.Length == 1 ? blendIndicesElements[0] : (MdlStructs.VertexElement?)null,
            blendWeightsElements.Length == 1 ? blendWeightsElements[0] : (MdlStructs.VertexElement?)null,
            uvElements,
            uvChannelCount);
        return true;
    }

    private static bool TryBuildUvElements(
        IReadOnlyList<MdlStructs.VertexElement> elements,
        out List<UvElementPacking> uvElements,
        out int uvChannelCount,
        out string? reason)
    {
        uvElements = [];
        uvChannelCount = 0;
        reason = null;

        var uvList = elements
            .Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.UV)
            .OrderBy(static e => e.UsageIndex)
            .ToList();

        foreach (var element in uvList)
        {
            var type = (MdlFile.VertexType)element.Type;
            if (type == MdlFile.VertexType.Half2 || type == MdlFile.VertexType.Single2)
            {
                if (uvChannelCount + 1 > Mesh.UVChannelCount)
                {
                    reason = "Too many UV channels.";
                    return false;
                }

                uvElements.Add(new UvElementPacking(element, uvChannelCount, null));
                uvChannelCount += 1;
            }
            else if (type == MdlFile.VertexType.Half4 || type == MdlFile.VertexType.Single4)
            {
                if (uvChannelCount + 2 > Mesh.UVChannelCount)
                {
                    reason = "Too many UV channels.";
                    return false;
                }

                uvElements.Add(new UvElementPacking(element, uvChannelCount, uvChannelCount + 1));
                uvChannelCount += 2;
            }
            else
            {
                reason = "Unsupported UV type.";
                return false;
            }
        }

        return true;
    }

    private static bool HasShapeData(MdlFile mdl)
        => mdl.Shapes.Length > 0
            || mdl.ShapeMeshes.Length > 0
            || mdl.ShapeValues.Length > 0
            || mdl.NeckMorphs.Length > 0;

    private static Vector3d ReadPosition(MdlFile.VertexType type, BinaryReader reader)
    {
        switch (type)
        {
            case MdlFile.VertexType.Single3:
                return new Vector3d(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
            case MdlFile.VertexType.Single4:
                var x = reader.ReadSingle();
                var y = reader.ReadSingle();
                var z = reader.ReadSingle();
                _ = reader.ReadSingle();
                return new Vector3d(x, y, z);
            default:
                throw new InvalidOperationException($"Unsupported position type {type}");
        }
    }

    private static Vector3 ReadNormal(MdlFile.VertexType type, BinaryReader reader)
    {
        switch (type)
        {
            case MdlFile.VertexType.Single3:
                return new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
            case MdlFile.VertexType.Single4:
                var x = reader.ReadSingle();
                var y = reader.ReadSingle();
                var z = reader.ReadSingle();
                _ = reader.ReadSingle();
                return new Vector3(x, y, z);
            case MdlFile.VertexType.NByte4:
                return ReadNByte4(reader).ToVector3();
            default:
                throw new InvalidOperationException($"Unsupported normal type {type}");
        }
    }

    private static Vector4 ReadTangent(MdlFile.VertexType type, BinaryReader reader)
    {
        return type switch
        {
            MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
            MdlFile.VertexType.NByte4 => ReadNByte4(reader),
            _ => throw new InvalidOperationException($"Unsupported tangent type {type}"),
        };
    }

    private static Vector4 ReadColor(MdlFile.VertexType type, BinaryReader reader)
    {
        return type switch
        {
            MdlFile.VertexType.UByte4 => ReadUByte4(reader),
            MdlFile.VertexType.NByte4 => ReadUByte4(reader),
            MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
            _ => throw new InvalidOperationException($"Unsupported color type {type}"),
        };
    }

    private static void ReadUv(MdlFile.VertexType type, BinaryReader reader, UvElementPacking mapping, Vector2[][] uvChannels, int vertexIndex)
    {
        if (type == MdlFile.VertexType.Half2 || type == MdlFile.VertexType.Single2)
        {
            var uv = type == MdlFile.VertexType.Half2
                ? new Vector2(ReadHalf(reader), ReadHalf(reader))
                : new Vector2(reader.ReadSingle(), reader.ReadSingle());

            uvChannels[mapping.FirstChannel][vertexIndex] = uv;
            return;
        }

        if (type == MdlFile.VertexType.Half4 || type == MdlFile.VertexType.Single4)
        {
            var uv = type == MdlFile.VertexType.Half4
                ? new Vector4(ReadHalf(reader), ReadHalf(reader), ReadHalf(reader), ReadHalf(reader))
                : new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());

            uvChannels[mapping.FirstChannel][vertexIndex] = new Vector2(uv.x, uv.y);
            if (mapping.SecondChannel.HasValue)
            {
                uvChannels[mapping.SecondChannel.Value][vertexIndex] = new Vector2(uv.z, uv.w);
            }
        }
    }

    private static byte[] ReadIndices(MdlFile.VertexType type, BinaryReader reader)
    {
        return type switch
        {
            MdlFile.VertexType.UByte4 => new[] { reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte() },
            _ => throw new InvalidOperationException($"Unsupported indices type {type}"),
        };
    }

    private static float[] ReadWeights(MdlFile.VertexType type, BinaryReader reader)
    {
        return type switch
        {
            MdlFile.VertexType.UByte4 => ReadUByte4(reader).ToFloatArray(),
            MdlFile.VertexType.NByte4 => ReadUByte4(reader).ToFloatArray(),
            MdlFile.VertexType.Single4 => new[] { reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle() },
            _ => throw new InvalidOperationException($"Unsupported weights type {type}"),
        };
    }

    private static Vector4 ReadUByte4(BinaryReader reader)
    {
        return new Vector4(
            reader.ReadByte() / 255f,
            reader.ReadByte() / 255f,
            reader.ReadByte() / 255f,
            reader.ReadByte() / 255f);
    }

    private static Vector4 ReadNByte4(BinaryReader reader)
    {
        var value = ReadUByte4(reader);
        return (value * 2f) - new Vector4(1f, 1f, 1f, 1f);
    }

    private static Vector4 ReadAndDiscard(MdlFile.VertexType type, BinaryReader reader)
    {
        return type switch
        {
            MdlFile.VertexType.Single2 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), 0, 0),
            MdlFile.VertexType.Single3 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0),
            MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
            MdlFile.VertexType.Half2 => new Vector4(ReadHalf(reader), ReadHalf(reader), 0, 0),
            MdlFile.VertexType.Half4 => new Vector4(ReadHalf(reader), ReadHalf(reader), ReadHalf(reader), ReadHalf(reader)),
            MdlFile.VertexType.UByte4 => ReadUByte4(reader),
            MdlFile.VertexType.NByte4 => ReadUByte4(reader),
            _ => Vector4.zero,
        };
    }

    private static void WritePosition(MdlFile.VertexType type, Vector3d value, Span<byte> target)
    {
        WriteVector3(type, new Vector3((float)value.x, (float)value.y, (float)value.z), target);
    }

    private static void WriteNormal(MdlFile.VertexType type, Vector3 value, Span<byte> target)
    {
        WriteVector3(type, value, target, normalized: type == MdlFile.VertexType.NByte4);
    }

    private static void WriteTangent(MdlFile.VertexType type, Vector4 value, Span<byte> target)
    {
        if (type == MdlFile.VertexType.NByte4)
        {
            WriteNByte4(value, target);
            return;
        }

        WriteVector4(type, value, target);
    }

    private static void WriteColor(MdlFile.VertexType type, Vector4 value, Span<byte> target)
    {
        if (type == MdlFile.VertexType.Single4)
        {
            WriteVector4(type, value, target);
            return;
        }

        WriteUByte4(value, target);
    }

    private static void WriteBlendIndices(MdlFile.VertexType type, BoneWeight weights, Span<byte> target)
    {
        if (type != MdlFile.VertexType.UByte4)
        {
            return;
        }

        target[0] = (byte)Math.Clamp(weights.boneIndex0, 0, 255);
        target[1] = (byte)Math.Clamp(weights.boneIndex1, 0, 255);
        target[2] = (byte)Math.Clamp(weights.boneIndex2, 0, 255);
        target[3] = (byte)Math.Clamp(weights.boneIndex3, 0, 255);
    }

    private static void WriteBlendWeights(MdlFile.VertexType type, BoneWeight weights, Span<byte> target)
    {
        if (type != MdlFile.VertexType.UByte4 && type != MdlFile.VertexType.NByte4)
        {
            if (type == MdlFile.VertexType.Single4)
            {
                BinaryPrimitives.WriteSingleLittleEndian(target[..4], weights.boneWeight0);
                BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), weights.boneWeight1);
                BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), weights.boneWeight2);
                BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), weights.boneWeight3);
            }
            return;
        }

        var w0 = Clamp01(weights.boneWeight0);
        var w1 = Clamp01(weights.boneWeight1);
        var w2 = Clamp01(weights.boneWeight2);
        var w3 = Clamp01(weights.boneWeight3);
        NormalizeWeights(ref w0, ref w1, ref w2, ref w3);

        target[0] = ToByte(w0);
        target[1] = ToByte(w1);
        target[2] = ToByte(w2);
        target[3] = ToByte(w3);
    }

    private static void WriteUv(MdlFile.VertexType type, UvElementPacking mapping, Vector2[][] uvChannels, int vertexIndex, Span<byte> target)
    {
        if (type == MdlFile.VertexType.Half2 || type == MdlFile.VertexType.Single2)
        {
            var uv = uvChannels[mapping.FirstChannel][vertexIndex];
            WriteVector2(type, uv, target);
            return;
        }

        if (type == MdlFile.VertexType.Half4 || type == MdlFile.VertexType.Single4)
        {
            var uv0 = uvChannels[mapping.FirstChannel][vertexIndex];
            var uv1 = mapping.SecondChannel.HasValue
                ? uvChannels[mapping.SecondChannel.Value][vertexIndex]
                : Vector2.zero;
            WriteVector4(type, new Vector4(uv0.x, uv0.y, uv1.x, uv1.y), target);
        }
    }

    private static void WriteVector2(MdlFile.VertexType type, Vector2 value, Span<byte> target)
    {
        if (type == MdlFile.VertexType.Single2)
        {
            BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y);
            return;
        }

        if (type == MdlFile.VertexType.Half2)
        {
            WriteHalf(target[..2], value.x);
            WriteHalf(target.Slice(2, 2), value.y);
        }
    }

    private static void WriteVector3(MdlFile.VertexType type, Vector3 value, Span<byte> target, bool normalized = false)
    {
        if (type == MdlFile.VertexType.Single3)
        {
            BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z);
            return;
        }

        if (type == MdlFile.VertexType.Single4)
        {
            BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), 1f);
            return;
        }

        if (type == MdlFile.VertexType.NByte4 && normalized)
        {
            WriteNByte4(new Vector4(value.x, value.y, value.z, 0f), target);
        }
    }

    private static void WriteVector4(MdlFile.VertexType type, Vector4 value, Span<byte> target)
    {
        if (type == MdlFile.VertexType.Single4)
        {
            BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z);
            BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), value.w);
            return;
        }

        if (type == MdlFile.VertexType.Half4)
        {
            WriteHalf(target[..2], value.x);
            WriteHalf(target.Slice(2, 2), value.y);
            WriteHalf(target.Slice(4, 2), value.z);
            WriteHalf(target.Slice(6, 2), value.w);
            return;
        }
    }

    private static void WriteUByte4(Vector4 value, Span<byte> target)
    {
        target[0] = ToByte(Clamp01(value.x));
        target[1] = ToByte(Clamp01(value.y));
        target[2] = ToByte(Clamp01(value.z));
        target[3] = ToByte(Clamp01(value.w));
    }

    private static void WriteNByte4(Vector4 value, Span<byte> target)
    {
        var normalized = (value * 0.5f) + new Vector4(0.5f);
        WriteUByte4(normalized, target);
    }

    private static void WriteHalf(Span<byte> target, float value)
    {
        var half = (Half)value;
        BinaryPrimitives.WriteUInt16LittleEndian(target, BitConverter.HalfToUInt16Bits(half));
    }

    private static float ReadHalf(BinaryReader reader)
        => (float)BitConverter.UInt16BitsToHalf(reader.ReadUInt16());

    private static float Clamp01(float value)
        => Math.Clamp(value, 0f, 1f);

    private static byte ToByte(float value)
        => (byte)Math.Clamp((int)Math.Round(value * 255f), 0, 255);

    private static void NormalizeWeights(float[] weights)
    {
        var sum = weights.Sum();
        if (sum <= float.Epsilon)
        {
            return;
        }

        for (var i = 0; i < weights.Length; i++)
        {
            weights[i] /= sum;
        }
    }

    private static void NormalizeWeights(ref float w0, ref float w1, ref float w2, ref float w3)
    {
        var sum = w0 + w1 + w2 + w3;
        if (sum <= float.Epsilon)
        {
            return;
        }

        w0 /= sum;
        w1 /= sum;
        w2 /= sum;
        w3 /= sum;
    }

    private static int GetElementSize(MdlFile.VertexType type)
        => type switch
        {
            MdlFile.VertexType.Single2 => 8,
            MdlFile.VertexType.Single3 => 12,
            MdlFile.VertexType.Single4 => 16,
            MdlFile.VertexType.Half2 => 4,
            MdlFile.VertexType.Half4 => 8,
            MdlFile.VertexType.UByte4 => 4,
            MdlFile.VertexType.NByte4 => 4,
            _ => throw new InvalidOperationException($"Unsupported vertex type {type}"),
        };

    private readonly record struct ElementKey(byte Stream, byte Offset, byte Type, byte Usage, byte UsageIndex)
    {
        public static ElementKey From(MdlStructs.VertexElement element)
            => new(element.Stream, element.Offset, element.Type, element.Usage, element.UsageIndex);
    }

    private sealed class VertexFormat
    {
        public VertexFormat(
            List<MdlStructs.VertexElement> sortedElements,
            MdlStructs.VertexElement? normalElement,
            MdlStructs.VertexElement? tangentElement,
            MdlStructs.VertexElement? colorElement,
            MdlStructs.VertexElement? blendIndicesElement,
            MdlStructs.VertexElement? blendWeightsElement,
            List<UvElementPacking> uvElements,
            int uvChannelCount)
        {
            SortedElements = sortedElements;
            NormalElement = normalElement;
            TangentElement = tangentElement;
            ColorElement = colorElement;
            BlendIndicesElement = blendIndicesElement;
            BlendWeightsElement = blendWeightsElement;
            UvElements = uvElements;
            UvChannelCount = uvChannelCount;
        }

        public List<MdlStructs.VertexElement> SortedElements { get; }
        public MdlStructs.VertexElement? NormalElement { get; }
        public MdlStructs.VertexElement? TangentElement { get; }
        public MdlStructs.VertexElement? ColorElement { get; }
        public MdlStructs.VertexElement? BlendIndicesElement { get; }
        public MdlStructs.VertexElement? BlendWeightsElement { get; }
        public List<UvElementPacking> UvElements { get; }
        public int UvChannelCount { get; }

        public bool HasNormals => NormalElement.HasValue;
        public bool HasTangents => TangentElement.HasValue;
        public bool HasColors => ColorElement.HasValue;
        public bool HasSkinning => BlendIndicesElement.HasValue && BlendWeightsElement.HasValue;
    }

    private readonly record struct UvElementPacking(MdlStructs.VertexElement Element, int FirstChannel, int? SecondChannel);

    private sealed class DecodedMeshData
    {
        public DecodedMeshData(
            Vector3d[] positions,
            Vector3[]? normals,
            Vector4[]? tangents,
            Vector4[]? colors,
            BoneWeight[]? boneWeights,
            Vector2[][]? uvChannels)
        {
            Positions = positions;
            Normals = normals;
            Tangents = tangents;
            Colors = colors;
            BoneWeights = boneWeights;
            UvChannels = uvChannels;
        }

        public Vector3d[] Positions { get; }
        public Vector3[]? Normals { get; }
        public Vector4[]? Tangents { get; }
        public Vector4[]? Colors { get; }
        public BoneWeight[]? BoneWeights { get; }
        public Vector2[][]? UvChannels { get; }
    }
}

internal static class MeshDecimatorVectorExtensions
{
    public static Vector3 ToVector3(this Vector4 value)
        => new(value.x, value.y, value.z);

    public static float[] ToFloatArray(this Vector4 value)
        => [value.x, value.y, value.z, value.w];
}
