Editor/Core/Import/Gltf/GltfDocument.cs
// Container and buffer handling adapted from humanoid-retargeter GltfDocument.cs.
#nullable enable annotations
using System.Text;
using System.Text.Json;
namespace HumanoidRigger.Formats.Gltf;
internal sealed class GltfDocument
{
    private const uint GlbMagic=0x46546C67,ChunkJson=0x4E4F534A,ChunkBin=0x004E4942;
    public JsonElement Root {get;}
    public List<byte[]> Buffers {get;}
    private GltfDocument(JsonElement root,List<byte[]> buffers){Root=root;Buffers=buffers;}
    public static GltfDocument Parse(byte[] data,Func<string,byte[]>? resolver=null)
    {
        var (json,bin)=data.Length>=4&&ReadU32(data,0)==GlbMagic?ParseGlbContainer(data):(data,(byte[]?)null);
        using var parsed=JsonDocument.Parse(Encoding.UTF8.GetString(json).TrimStart('\uFEFF'));
        var root=parsed.RootElement.Clone();
        if(!root.TryGetProperty("asset",out var asset)||asset.GetProperty("version").GetString()!="2.0")throw new FormatException("Use glTF 2.0 or GLB 2.0.");
        if(root.TryGetProperty("extensionsRequired",out var required))foreach(var extension in required.EnumerateArray())
            if(extension.GetString() is not ("KHR_mesh_quantization" or "KHR_materials_unlit" or "KHR_texture_transform"))
                throw new FormatException($"Required glTF extension '{extension.GetString()}' is unsupported. Export an uncompressed glTF/GLB with standard materials.");
        var buffers=ResolveBuffers(root,bin,resolver);
        if(buffers.Sum(b=>(long)b.Length)>ModelImporter.MaximumBytes)throw new FormatException("The glTF buffers exceed the 512 MB import limit.");
        if(root.TryGetProperty("buffers",out var definitions))for(int i=0;i<buffers.Count;i++)
        {
            int length=definitions[i].GetProperty("byteLength").GetInt32();
            if(length<0||length>buffers[i].Length)throw new FormatException("Truncated glTF buffer.");
            if(length!=buffers[i].Length)buffers[i]=buffers[i].AsSpan(0,length).ToArray();
        }
        return new(root,buffers);
    }
    public byte[] ViewBytes(int index)
    {
        var view=Root.GetProperty("bufferViews")[index];int buffer=view.GetProperty("buffer").GetInt32();
        int offset=view.TryGetProperty("byteOffset",out var o)?o.GetInt32():0,length=view.GetProperty("byteLength").GetInt32();
        if(buffer<0||buffer>=Buffers.Count||offset<0||length<0||(long)offset+length>Buffers[buffer].Length)throw new FormatException("Invalid glTF image bufferView.");
        return Buffers[buffer].AsSpan(offset,length).ToArray();
    }
    private static (byte[] Json, byte[]? Bin) ParseGlbContainer(byte[] data)
    {
        if (data.Length < 12)
            throw new FormatException("GLB: truncated header (need 12 bytes).");

        uint version = ReadU32(data, 4);
        if (version != 2)
            throw new FormatException($"GLB: unsupported container version {version} (expected 2).");

        long declared = ReadU32(data, 8);
        if (declared != data.Length)
            throw new FormatException(
                $"GLB: truncated file (header declares {declared} bytes, got {data.Length}).");

        byte[]? json = null, bin = null;
        long offset = 12;
        while (offset + 8 <= declared)
        {
            long length = ReadU32(data, (int)offset);
            uint type = ReadU32(data, (int)offset + 4);
            offset += 8;
            if (offset + length > declared || length%4!=0)
                throw new FormatException("GLB: truncated chunk (declared length exceeds the file).");

            if(offset==20&&type!=ChunkJson||type==ChunkJson&&json is not null||type==ChunkBin&&bin is not null)
                throw new FormatException("GLB: invalid chunk order or duplicate chunk.");

            if (type == ChunkJson && json is null)
                json = data.AsSpan((int)offset, (int)length).ToArray();
            else if (type == ChunkBin && bin is null)
                bin = data.AsSpan((int)offset, (int)length).ToArray();
            // Unknown chunk types are skipped per spec.

            offset += length;
        }

        if (json is null || offset!=declared)
            throw new FormatException("GLB: no JSON chunk found.");
        return (json, bin);
    }

    private static uint ReadU32(byte[] data, int offset)
        => (uint)(data[offset] | data[offset + 1] << 8 | data[offset + 2] << 16 | data[offset + 3] << 24);

    // ================================================================== buffers

    /// <summary>
    /// Resolves every entry of <c>buffers</c>: no <c>uri</c> = the GLB BIN chunk (spec: only
    /// buffer 0 may do this), <c>data:</c> URIs are base64-decoded inline. External file
    /// URIs are delegated to the optional caller-supplied resolver so this core remains
    /// independent of the file system.
    /// </summary>
    private static List<byte[]> ResolveBuffers(
        JsonElement root, byte[]? bin, Func<string, byte[]>? externalBufferResolver)
    {
        var buffers = new List<byte[]>();
        if (!root.TryGetProperty("buffers", out var array) || array.ValueKind != JsonValueKind.Array)
            return buffers;

        foreach (var buffer in array.EnumerateArray())
        {
            if (!buffer.TryGetProperty("uri", out var uriProp))
            {
                if(buffers.Count!=0)throw new FormatException("Only GLB buffer 0 may omit its URI.");
                buffers.Add(bin ?? throw new FormatException(
                    "glTF: buffer has no uri but the file has no GLB BIN chunk."));
                continue;
            }

            var uri = uriProp.GetString() ?? "";
            if (uri.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
            {
                int comma = uri.IndexOf(',');
                if (comma < 0 || !uri[..comma].EndsWith(";base64", StringComparison.OrdinalIgnoreCase))
                    throw new FormatException("glTF: only base64 data: URIs are supported for buffers.");
                try
                {
                    buffers.Add(Convert.FromBase64String(uri[(comma + 1)..]));
                }
                catch (Exception e) when (e is FormatException or ArgumentException)
                {
                    throw new FormatException("glTF: invalid base64 in buffer data: URI.");
                }
            }
            else
            {
                if (externalBufferResolver is null)
                {
                    throw new FormatException(
                        $"glTF: buffer references an external file ('{uri}') which this importer cannot "
                        + "read (no file IO). Export as .glb (binary, self-contained) instead.");
                }
                try
                {
                    buffers.Add(externalBufferResolver(uri) ?? throw new FormatException(
                        $"glTF: external buffer resolver returned no data for '{uri}'."));
                }
                catch (FormatException)
                {
                    throw;
                }
                catch (Exception e)
                {
                    throw new FormatException(
                        $"glTF: could not read external buffer '{uri}' ({e.Message}).");
                }
            }
        }
        return buffers;
    }

}