Editor/UI/AssetOutput.cs
using Editor;
using Sandbox;
using System.Threading.Tasks;
namespace HumanoidRigger.Editor;

public sealed record ExportResult(string[] Files)
{
    public string PrimaryFile=>Files.LastOrDefault(p=>p.EndsWith(".vmdl",StringComparison.OrdinalIgnoreCase))??Files.First();
}

public static class AssetOutput
{
    public static async Task<string> Save(Wizard session)=>
        (await Save(session,ExportRequest.Default(session.Character!.Name,Project.Current.GetAssetsPath(),session.Character.Materials.Length>0))).PrimaryFile;

    public static async Task<ExportResult> Save(Wizard session,ExportRequest request)
    {
        if(session.Rig?.Report.Passed!=true)throw new InvalidOperationException("The rig must pass validation before saving.");
        var character=session.Character!;var rig=session.Rig;
        if(request.Formats.HasFlag(ExportFormats.Vmdl))VmdlBoneNames.Validate(rig.Bones);
        var root=Project.Current.GetAssetsPath();var plan=request.Plan(root,character.Materials.Length>0);plan.EnsureAvailable();
        Directory.CreateDirectory(plan.Directory);
        var written=new List<string>();bool createdMaterials=false;
        void Write(string path,byte[] bytes)
        {
            using var file=new FileStream(path,FileMode.CreateNew,FileAccess.Write,FileShare.Read);written.Add(path);file.Write(bytes);
        }
        void WriteText(string path,string content)=>Write(path,System.Text.Encoding.UTF8.GetBytes(content));
        try
        {
            var sourceMaterials=character.Materials;
            IReadOnlyDictionary<int,string> vmats=new Dictionary<int,string>();
            if(plan.MaterialDirectory is not null)
            {
                Directory.CreateDirectory(plan.MaterialDirectory);createdMaterials=true;
                sourceMaterials=await Task.Run(()=>TextureFiles.Copy(character,plan.MaterialDirectory));await new EditorThread();
                sourceMaterials=await Task.Run(()=>MaterialAssets.PrepareFormats(sourceMaterials,plan.MaterialDirectory,plan.Formats));await new EditorThread();
                if(plan.Formats.HasFlag(ExportFormats.Vmdl))
                {
                    vmats=await MaterialAssets.Compile(sourceMaterials,plan.MaterialDirectory,character);
                }
            }
            var portableMaterials=plan.MaterialDirectory is null?sourceMaterials:TextureFiles.RelativeTo(sourceMaterials,Path.GetFileName(plan.MaterialDirectory));
            if(plan.Formats.HasFlag(ExportFormats.Fbx))
            {
                var bytes=await Task.Run(()=>FbxExporter.Write(character,rig,portableMaterials));await new EditorThread();
                Write(plan.PathFor(".fbx"),bytes);
                if(ExportRequest.IsInAssets(plan.Directory,root))AssetSystem.RegisterFile(plan.PathFor(".fbx"));
            }
            if(plan.Formats.HasFlag(ExportFormats.Gltf))
            {
                var output=await Task.Run(()=>GltfExporter.Write(character,rig,plan.FileName+".bin",portableMaterials));await new EditorThread();
                Write(plan.PathFor(".bin"),output.Buffer);Write(plan.PathFor(".gltf"),output.Document);
            }
            if(plan.Formats.HasFlag(ExportFormats.Glb))
            {
                var output=await Task.Run(()=>GltfExporter.Write(character,rig,"",portableMaterials,true,path=>File.ReadAllBytes(Path.Combine(plan.Directory,path))));await new EditorThread();
                Write(plan.PathFor(".glb"),output.Document);
            }
            if(plan.Formats.HasFlag(ExportFormats.Obj))
            {
                var output=await Task.Run(()=>ObjExporter.Write(character,plan.FileName+".mtl",portableMaterials));await new EditorThread();
                WriteText(plan.PathFor(".mtl"),output.Materials);WriteText(plan.PathFor(".obj"),output.Mesh);
            }
            if(plan.Formats.HasFlag(ExportFormats.Vmdl))
            {
                var dmx=await Task.Run(()=>DmxExporter.Write(character,rig,vmats));await new EditorThread();
                WriteText(plan.PathFor(".dmx"),dmx);
                WriteText(plan.PathFor(".vmdl"),ModelDocExporter.Write(Path.GetRelativePath(root,plan.PathFor(".dmx")),rig,character,vmats.Count>0));
                await NativeRigExport.Compile(plan.PathFor(".dmx"),plan.PathFor(".vmdl"),character,rig,vmats,
                    content=>File.WriteAllText(plan.PathFor(".dmx"),content));
            }
            return new(plan.PrimaryFiles);
        }
        catch
        {
            // Only remove output paths this attempt created. Existing user assets are never overwritten.
            foreach(var path in written)foreach(var owned in new[]{path,path+"_c"})try{File.Delete(owned);}catch(Exception e){Log.Warning(e.Message);}
            if(createdMaterials&&ExportRequest.IsInAssets(plan.MaterialDirectory,plan.Directory))
                try{Directory.Delete(plan.MaterialDirectory,true);}catch(Exception e){Log.Warning(e.Message);}
            throw;
        }
    }
}