Editor/Core/Export/ExportRequest.cs
#nullable enable annotations
namespace HumanoidRigger;

[Flags]
public enum ExportFormats { None=0, Fbx=1, Vmdl=2, Gltf=4, Glb=8, Obj=16, Both=Fbx|Vmdl, All=Fbx|Vmdl|Gltf|Glb|Obj }

public sealed record ExportRequest(string FileName,string Directory,ExportFormats Formats=ExportFormats.Both)
{
    public ExportPlan Plan(string assetsPath,bool hasMaterials)
    {
        if(Formats==ExportFormats.None||(Formats&~ExportFormats.All)!=0)throw new FormatException("Select at least one export format.");
        var name=FileName.Trim();
        if(Path.GetExtension(name).ToLowerInvariant() is ".fbx" or ".vmdl" or ".gltf" or ".glb" or ".obj")name=Path.GetFileNameWithoutExtension(name);
        if(string.IsNullOrWhiteSpace(name)||name is "." or ".."||name.EndsWith('.')||name.EndsWith(' ')||name.IndexOfAny(Path.GetInvalidFileNameChars())>=0)
            throw new FormatException("Enter a valid filename without a folder path.");
        var device=name.Split('.')[0].ToUpperInvariant();
        if(device is "CON" or "PRN" or "AUX" or "NUL"||Enumerable.Range(1,9).Any(n=>device=="COM"+n||device=="LPT"+n))throw new FormatException("That filename is reserved by Windows.");
        if(string.IsNullOrWhiteSpace(Directory))throw new FormatException("Choose a destination folder.");
        var folder=Path.GetFullPath(Path.IsPathFullyQualified(Directory)?Directory:Path.Combine(assetsPath,Directory));
        if(Formats.HasFlag(ExportFormats.Vmdl)&&!IsInAssets(folder,assetsPath))throw new FormatException("Choose a folder inside this project's Assets folder to save Vmdl files.");
        var files=new List<string>();
        if(Formats.HasFlag(ExportFormats.Fbx))files.Add(Path.Combine(folder,name+".fbx"));
        if(Formats.HasFlag(ExportFormats.Gltf))files.AddRange([Path.Combine(folder,name+".gltf"),Path.Combine(folder,name+".bin")]);
        if(Formats.HasFlag(ExportFormats.Glb))files.Add(Path.Combine(folder,name+".glb"));
        if(Formats.HasFlag(ExportFormats.Obj))files.AddRange([Path.Combine(folder,name+".obj"),Path.Combine(folder,name+".mtl")]);
        if(Formats.HasFlag(ExportFormats.Vmdl))files.AddRange([Path.Combine(folder,name+".dmx"),Path.Combine(folder,name+".vmdl")]);
        return new(name,folder,Formats,files.ToArray(),hasMaterials?Path.Combine(folder,name+"_materials"):null);
    }
    public static bool IsInAssets(string path,string assetsPath)
    {
        var relative=Path.GetRelativePath(Path.GetFullPath(assetsPath),Path.GetFullPath(path));
        return !Path.IsPathRooted(relative)&&relative!=".."&&!relative.StartsWith(".."+Path.DirectorySeparatorChar);
    }
    public static ExportRequest Default(string characterName,string assetsPath,bool hasMaterials)
    {
        var name=new string(characterName.Select(c=>char.IsLetterOrDigit(c)||c=='_'||c=='-'?c:'_').ToArray());
        if(name.Length==0)name="character";
        var folder=Path.Combine(assetsPath,"humanoid_rigger");
        if(File.Exists(folder))return new(name,folder);
        for(int i=0;;i++)
        {
            var request=new ExportRequest(i==0?name:name+"_"+i,folder);
            try{request.Plan(assetsPath,hasMaterials).EnsureAvailable();return request;}
            catch(IOException){}
            catch(FormatException){name="character";}
        }
    }
}

public sealed record ExportPlan(string FileName,string Directory,ExportFormats Formats,string[] Files,string? MaterialDirectory)
{
    public string[] PrimaryFiles=>Files.Where(p=>Path.GetExtension(p) is ".fbx" or ".vmdl" or ".gltf" or ".glb" or ".obj").ToArray();
    public string PathFor(string extension)=>Path.Combine(Directory,FileName+extension);
    public void EnsureAvailable()
    {
        if(File.Exists(Directory))throw new IOException("The destination is a file. Choose a folder.");
        foreach(var path in Files.Concat(MaterialDirectory is null?[]:new[]{MaterialDirectory}))
            if(File.Exists(path)||System.IO.Directory.Exists(path)||File.Exists(path+"_c"))throw new IOException($"'{Path.GetFileName(path)}' already exists. Choose another filename or folder.");
    }
}