Editor/Core/Import/ModelImporter.cs
#nullable enable annotations
namespace HumanoidRigger;
/// <summary>All pickers and file drops enter the same format-neutral import pipeline.</summary>
public static class ModelImporter
{
    public const int MaximumBytes=512*1024*1024;
    public const string FileFilter="Character files (*.fbx *.obj *.gltf *.glb)";
    public static bool CanImport(string path)=>Path.GetExtension(path).ToLowerInvariant() is ".fbx" or ".obj" or ".gltf" or ".glb";
    public static ImportedCharacter Import(string path)
    {
        if(!CanImport(path))throw new FormatException("Choose an Fbx, Obj, Gltf or Glb character.");
        if(new FileInfo(path).Length>MaximumBytes)throw new FormatException("The model exceeds the 512 MB import limit.");
        return Import(File.ReadAllBytes(path),Path.GetFileNameWithoutExtension(path),Path.GetFullPath(path));
    }
    public static ImportedCharacter Import(byte[] bytes,string name,string sourcePath="")
    {
        if(bytes.Length>MaximumBytes)throw new FormatException("The model exceeds the 512 MB import limit.");
        var extension=Path.GetExtension(sourcePath).ToLowerInvariant();
        try
        {
            var model=extension switch
            {
                ".obj"=>ObjImporter.Import(bytes,name,sourcePath),
                ".gltf" or ".glb"=>Formats.Gltf.GltfMeshImporter.Import(bytes,name,sourcePath),
                _=>FbxModelImporter.Import(bytes,name,sourcePath)
            };
            return HumanoidFacing.Normalize(model);
        }
        catch(System.Text.Json.JsonException e){throw new FormatException("Invalid glTF JSON: "+e.Message,e);}
        catch(Exception e) when(e is IndexOutOfRangeException or KeyNotFoundException or OverflowException)
        {throw new FormatException("The model contains an invalid index, offset or array length.",e);}
    }
    internal static byte[] ReadDependency(string modelPath,string uri)
    {
        if(uri.StartsWith("data:",StringComparison.OrdinalIgnoreCase))
        {
            int comma=uri.IndexOf(',');
            if(comma<0||!uri[..comma].EndsWith(";base64",StringComparison.OrdinalIgnoreCase))throw new FormatException("Only base64 embedded resources are supported.");
            var data=Convert.FromBase64String(uri[(comma+1)..]);
            if(data.Length>MaximumBytes)throw new FormatException("Embedded resource exceeds the import limit.");
            return data;
        }
        string path=DependencyPath(modelPath,uri);
        if(new FileInfo(path).Length>MaximumBytes)throw new FormatException("Model dependency exceeds the import limit.");
        return File.ReadAllBytes(path);
    }
    internal static string DependencyPath(string modelPath,string uri)
    {
        string relative=Uri.UnescapeDataString(uri).Replace('/',Path.DirectorySeparatorChar);
        if(string.IsNullOrEmpty(modelPath)||Path.IsPathRooted(relative)||relative.Contains(':'))throw new FormatException("Model dependencies must use local relative paths.");
        return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(modelPath)!,relative));
    }
    internal static MeshKind Classify(string name)
    {
        var s=name.ToLowerInvariant();
        if(new[]{"weapon","sword","backpack","hat","prop"}.Any(s.Contains)) return MeshKind.Accessory;
        if(new[]{"hair","beard"}.Any(s.Contains)) return MeshKind.Hair;
        if(new[]{"boot","shoe"}.Any(s.Contains)) return MeshKind.Shoe;
        if(new[]{"coat","cloth","shirt","pants","jacket"}.Any(s.Contains)) return MeshKind.Clothing;
        return MeshKind.Body;
    }
}