Editor/Core/Import/Gltf/GltfMaterials.cs
#nullable enable annotations
using System.Text.Json;
namespace HumanoidRigger.EditorTools.Core.Import.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()];
            var webp=Property(Property(texture,"extensions"),"EXT_texture_webp");
            int index=(webp.ValueKind==JsonValueKind.Object?webp: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 sg=Property(Property(m,"extensions"),"KHR_materials_pbrSpecularGlossiness");
            // The extension takes precedence; unused fallback maps must not
            // produce missing-image warnings or require another image decoder.
            if(sg.ValueKind==JsonValueKind.Object)pbr=default;
            var factor=Property(pbr,"baseColorFactor");var emissive=Property(m,"emissiveFactor");
            string alpha=m.TryGetProperty("alphaMode",out var mode)?mode.GetString()!:"OPAQUE";
            var result=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")),
                NormalScale=Float(Property(m,"normalTexture"),"scale",1),OcclusionStrength=Float(Property(m,"occlusionTexture"),"strength",1),
                EmissiveFactor=emissive.ValueKind==JsonValueKind.Array?new Vector3(emissive[0].GetSingle(),emissive[1].GetSingle(),emissive[2].GetSingle()):Vector3.Zero,
                EmissiveStrength=Float(Property(Property(m,"extensions"),"KHR_materials_emissive_strength"),"emissiveStrength",1),
                DoubleSided=m.TryGetProperty("doubleSided",out var two)&&two.GetBoolean(),AlphaTest=alpha=="MASK",Translucent=alpha=="BLEND",AlphaCutoff=Float(m,"alphaCutoff",.5f)};
            if(!float.IsFinite(result.EmissiveStrength)||result.EmissiveStrength<0||!float.IsFinite(result.NormalScale)||
                !float.IsFinite(result.OcclusionStrength)||result.OcclusionStrength<0||result.OcclusionStrength>1)
                throw new FormatException($"Material '{result.Name}' has an invalid emission, normal or occlusion strength.");
            if(sg.ValueKind==JsonValueKind.Object)
            {
                var diffuse=Property(sg,"diffuseFactor");var specular=Property(sg,"specularFactor");
                var source=new SpecularGlossinessMaterial{
                    DiffuseFactor=diffuse.ValueKind==JsonValueKind.Array?new(diffuse[0].GetSingle(),diffuse[1].GetSingle(),diffuse[2].GetSingle(),diffuse[3].GetSingle()):System.Numerics.Vector4.One,
                    SpecularFactor=specular.ValueKind==JsonValueKind.Array?new(specular[0].GetSingle(),specular[1].GetSingle(),specular[2].GetSingle()):Vector3.One,
                    GlossinessFactor=Float(sg,"glossinessFactor",1),DiffuseTexture=Texture(Property(sg,"diffuseTexture")),SpecularGlossinessTexture=Texture(Property(sg,"specularGlossinessTexture"))};
                var converted=source.Evaluate(System.Numerics.Vector4.One,System.Numerics.Vector4.One);
                result.SpecularGlossiness=source;result.ColorTexture=source.DiffuseTexture;result.ColorFactor=converted.Color;result.OpacityFactor=converted.Opacity;
                result.MetallicRoughnessTexture=null;result.MetallicFactor=converted.Metallic;result.RoughnessFactor=converted.Roughness;
            }
            return result;
        }).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");
        var sg=Property(Property(material,"extensions"),"KHR_materials_pbrSpecularGlossiness");
        var workflow=sg.ValueKind==JsonValueKind.Object?sg:pbr;
        foreach(string name in sg.ValueKind==JsonValueKind.Object?new[]{"diffuseTexture","specularGlossinessTexture"}:new[]{"baseColorTexture","metallicRoughnessTexture"})
        {var value=Property(workflow,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];
    }
}