Editor/HumanoidHandRetargeter/HandMaterialImport.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Editor;
using HumanoidHandRetargeter.Formats.Fbx;
using Sandbox;
namespace HumanoidHandRetargeter.Editor;

/// <summary>Preserves FBX material links and resolves moved texture sets without guessing a different material.</summary>
public static class HandMaterialImport
{
    public static IReadOnlyDictionary<string,string> Write(FbxMaterialAssets.Prepared prepared,string folder)
    {
        var paths=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
        foreach(var (reference,file) in prepared.Textures)
        {
            var stem=Convert.ToHexString(SHA256.HashData(file.Bytes)).ToLowerInvariant()[..16];
            var relative=folder+"/textures/"+stem+file.Extension;var absolute=Path.Combine(HandEditorPipeline.Assets,relative);
            Directory.CreateDirectory(Path.GetDirectoryName(absolute)!);if(!File.Exists(absolute))File.WriteAllBytes(absolute,file.Bytes);
            if(file.Extension is ".jpeg" or ".webp")
            {
                using var bitmap=SkiaSharp.SKBitmap.Decode(file.Bytes)??throw new InvalidOperationException("Cannot decode "+reference);
                using var image=SkiaSharp.SKImage.FromBitmap(bitmap);using var encoded=image.Encode(SkiaSharp.SKEncodedImageFormat.Png,100);
                relative=folder+"/textures/"+stem+".png";absolute=Path.Combine(HandEditorPipeline.Assets,relative);if(!File.Exists(absolute))File.WriteAllBytes(absolute,encoded.ToArray());
            }
            AssetSystem.RegisterFile(absolute);paths.Add(reference,relative);
        }
        var remaps=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
        foreach(var material in prepared.Materials)
        {
            var materialId=Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(material.Name))).ToLowerInvariant()[..8];
            var path=folder+"/materials/"+HandEditorPipeline.SafeName(material.Name).ToLowerInvariant()+"_"+materialId+".vmat";
            var absolute=Path.Combine(HandEditorPipeline.Assets,path);Directory.CreateDirectory(Path.GetDirectoryName(absolute)!);
            if(!File.Exists(absolute))
            {
                var text=new StringBuilder("// Imported hand material. Authored edits are preserved.\nLayer0\n{\n");
                text.AppendLine(material.VertexColors&&material.ColorTexture is null?"shader \"shaders/vertex_color.shader\"":"shader \"shaders/complex.shader\"");
                void Texture(string parameter,string? reference,string? fallback=null){var value=reference is null?fallback:paths[reference];if(value is not null)text.AppendLine(parameter+" \""+value+"\"");}
                Texture("TextureColor",material.ColorTexture,"materials/default/default_color.tga");
                Texture("TextureNormal",material.NormalTexture,"materials/default/default_normal.tga");
                Texture("TextureRoughness",material.RoughnessTexture,"materials/default/default_rough.tga");
                Texture("TextureAmbientOcclusion",material.OcclusionTexture);
                if(material.MetalnessTexture is not null){text.AppendLine("F_METALNESS_TEXTURE 1");Texture("TextureMetalness",material.MetalnessTexture);}
                if(material.EmissiveTexture is not null){text.AppendLine("F_SELF_ILLUM 1");Texture("TextureSelfIllumMask",material.EmissiveTexture);}
                if(material.DoubleSided)text.AppendLine("F_RENDER_BACKFACES 1");
                var opacity=material.OpacityTexture is {} opacityReference?paths[opacityReference]:null;
                var colorPath=material.ColorTexture is {} colorReference?paths[colorReference]:null;
                if((material.AlphaTest||material.Translucent)&&opacity is null)opacity=colorPath;
                if(opacity is not null&&string.Equals(opacity,colorPath,StringComparison.OrdinalIgnoreCase))opacity=ExtractPackedAlpha(opacity);
                if(opacity is not null)
                {
                    if(material.AlphaTest)
                    {
                        text.AppendLine("F_ALPHA_TEST 1");
                        text.AppendLine(FormattableString.Invariant($"g_flAlphaTestReference {material.AlphaCutoff:R}"));
                    }
                    else text.AppendLine("F_TRANSLUCENT 1");
                    text.AppendLine("TextureTranslucency \""+opacity+"\"");
                }
                if(material.ColorTexture is null && material.ColorFactor is {} color)text.AppendLine(FormattableString.Invariant($"g_vColorTint \"[{color.X:R} {color.Y:R} {color.Z:R} 1]\""));
                text.AppendLine("}");File.WriteAllText(absolute,text.ToString());
            }
            AssetSystem.RegisterFile(absolute);remaps.Add(material.Name.ToLowerInvariant()+".vmat",path);
        }
        return remaps;
    }

    // Same packed-alpha handling as humanoid-retargeter: complex.shader expects
    // grayscale opacity, not the RGB channels of a packed color texture.
    static string? ExtractPackedAlpha(string relative)
    {
        using var bitmap=SkiaSharp.SKBitmap.Decode(Path.Combine(HandEditorPipeline.Assets,relative))
            ??throw new InvalidOperationException("Cannot decode packed opacity texture '"+relative+"'. Supply a PNG export.");
        var pixels=bitmap.Pixels;
        if(pixels.All(p=>p.Alpha==255))return null;
        using var mask=new SkiaSharp.SKBitmap(bitmap.Width,bitmap.Height,SkiaSharp.SKColorType.Rgba8888,SkiaSharp.SKAlphaType.Opaque);
        mask.Pixels=pixels.Select(p=>new SkiaSharp.SKColor(p.Alpha,p.Alpha,p.Alpha)).ToArray();
        using var image=SkiaSharp.SKImage.FromBitmap(mask);
        using var data=image.Encode(SkiaSharp.SKEncodedImageFormat.Png,100);
        var output=relative+"_alpha.png";
        var absolute=Path.Combine(HandEditorPipeline.Assets,output);
        if(!File.Exists(absolute))File.WriteAllBytes(absolute,data.ToArray());
        AssetSystem.RegisterFile(absolute);
        return output;
    }
}