Editor/Core/Import/Gltf/GltfMaterials.cs
#nullable enable annotations
using System.Text.Json;
namespace HumanoidRigger.Formats.Gltf;
using Vector3=System.Numerics.Vector3;
using Vector2=System.Numerics.Vector2;

internal static class GltfMaterials
{
    public static SourceMaterial[] Read(GltfDocument document,string path,Dictionary<string,byte[]> embedded,List<string> warnings)
    {
        var root=document.Root;
        if(!root.TryGetProperty("materials",out var materials))return [];
        string? Texture(JsonElement info)
        {
            if(info.ValueKind!=JsonValueKind.Object)return null;
            var texture=root.GetProperty("textures")[info.GetProperty("index").GetInt32()];
            int index=texture.GetProperty("source").GetInt32();var image=root.GetProperty("images")[index];
            if(image.TryGetProperty("uri",out var uri)&&!uri.GetString()!.StartsWith("data:",StringComparison.OrdinalIgnoreCase))
            {
                string requested=ModelImporter.DependencyPath(path,uri.GetString()!);
                // Some exporters rename the URI but leave the image's original
                // filename in its name field. Use that exact sibling when present.
                if(!File.Exists(requested)&&image.TryGetProperty("name",out var label)&&Path.GetFileName(label.GetString())==label.GetString())
                {
                    string alternate=ModelImporter.DependencyPath(path,label.GetString()!);
                    if(File.Exists(alternate))
                    {
                        warnings.Add($"Texture '{uri.GetString()}' was recovered from '{label.GetString()}'.");
                        return alternate;
                    }
                }
                if(!File.Exists(requested))warnings.Add($"Texture '{uri.GetString()}' is missing.");
                return requested;
            }
            string mime=image.TryGetProperty("mimeType",out var type)?type.GetString()!:image.GetProperty("uri").GetString()!.Split(';')[0][5..];
            string extension=mime switch{"image/png"=>".png","image/jpeg"=>".jpg","image/webp"=>".webp",_=>throw new FormatException("Unsupported glTF image format: "+mime)};
            string name="gltf_image_"+index+extension;
            if(!embedded.ContainsKey(name))embedded[name]=image.TryGetProperty("bufferView",out var view)?document.ViewBytes(view.GetInt32()):ModelImporter.ReadDependency(path,image.GetProperty("uri").GetString()!);
            return name;
        }
        return materials.EnumerateArray().Select((m,i)=>
        {
            m.TryGetProperty("pbrMetallicRoughness",out var pbr);
            var factor=Property(pbr,"baseColorFactor");var emissive=Property(m,"emissiveFactor");
            string alpha=m.TryGetProperty("alphaMode",out var mode)?mode.GetString()!:"OPAQUE";
            return new SourceMaterial{Name=m.TryGetProperty("name",out var name)?name.GetString()!:"material_"+i,AuthoredPbr=true,Unlit=Property(Property(m,"extensions"),"KHR_materials_unlit").ValueKind==JsonValueKind.Object,
                ColorFactor=factor.ValueKind==JsonValueKind.Array?new Vector3(factor[0].GetSingle(),factor[1].GetSingle(),factor[2].GetSingle()):Vector3.One,
                OpacityFactor=factor.ValueKind==JsonValueKind.Array?factor[3].GetSingle():1,
                ColorTexture=Texture(Property(pbr,"baseColorTexture")),MetallicRoughnessTexture=Texture(Property(pbr,"metallicRoughnessTexture")),
                MetallicFactor=Float(pbr,"metallicFactor",1),RoughnessFactor=Float(pbr,"roughnessFactor",1),
                NormalTexture=Texture(Property(m,"normalTexture")),OcclusionTexture=Texture(Property(m,"occlusionTexture")),EmissiveTexture=Texture(Property(m,"emissiveTexture")),
                EmissiveFactor=emissive.ValueKind==JsonValueKind.Array?new Vector3(emissive[0].GetSingle(),emissive[1].GetSingle(),emissive[2].GetSingle()):Vector3.Zero,
                DoubleSided=m.TryGetProperty("doubleSided",out var two)&&two.GetBoolean(),AlphaTest=alpha=="MASK",Translucent=alpha=="BLEND",AlphaCutoff=Float(m,"alphaCutoff",.5f)};
        }).ToArray();
    }
    internal static JsonElement Property(JsonElement element,string name)=>element.ValueKind==JsonValueKind.Object&&element.TryGetProperty(name,out var value)?value:default;
    internal static float Float(JsonElement element,string name,float fallback)=>Property(element,name) is {ValueKind:JsonValueKind.Number} value?value.GetSingle():fallback;
    public static (int Set,Vector2 Offset,Vector2 Scale,float Rotation) UvTransform(JsonElement material)
    {
        var infos=new List<JsonElement>();var pbr=Property(material,"pbrMetallicRoughness");
        foreach(string name in new[]{"baseColorTexture","metallicRoughnessTexture"}){var value=Property(pbr,name);if(value.ValueKind==JsonValueKind.Object)infos.Add(value);}
        foreach(string name in new[]{"normalTexture","occlusionTexture","emissiveTexture"}){var value=Property(material,name);if(value.ValueKind==JsonValueKind.Object)infos.Add(value);}
        var transforms=infos.Select(info=>
        {
            var transform=Property(Property(info,"extensions"),"KHR_texture_transform");
            int set=Property(info,"texCoord") is {ValueKind:JsonValueKind.Number} uv?uv.GetInt32():0;
            if(Property(transform,"texCoord") is {ValueKind:JsonValueKind.Number} alternate)set=alternate.GetInt32();
            Vector2 Pair(string name,Vector2 fallback)=>Property(transform,name) is {ValueKind:JsonValueKind.Array} value?new(value[0].GetSingle(),value[1].GetSingle()):fallback;
            return(Set:set,Offset:Pair("offset",Vector2.Zero),Scale:Pair("scale",Vector2.One),Rotation:Float(transform,"rotation",0));
        }).Distinct().ToArray();
        if(transforms.Length>1)throw new FormatException("This material uses different UV sets or transforms per texture. Bake them to one UV set before importing.");
        return transforms.Length==0?(0,Vector2.Zero,Vector2.One,0):transforms[0];
    }
}