Editor/WeaponImporter/Core/Import/WeaponLoader.cs
#nullable enable annotations

using WeaponImporter.Core.Weapon;

namespace WeaponImporter.Core.Import;

/// <summary>An import failure with a message written for the person importing.</summary>
public sealed class WeaponImportException : Exception
{
    public WeaponImportException(string message, Exception? inner = null) : base(message, inner) { }
}

/// <summary>Loads FBX / glTF / GLB sources into a <see cref="WeaponAsset"/>.</summary>
public static class WeaponLoader
{
    public static readonly string[] SourceExtensions = { ".fbx", ".glb", ".gltf" };

    /// <summary>All formats the importer accepts, including engine models loaded in the editor.</summary>
    public static readonly string[] AllExtensions = { ".fbx", ".glb", ".gltf", ".vmdl" };

    public static bool IsSource(string path) => SourceExtensions.Contains(Path.GetExtension(path).ToLowerInvariant());
    public static bool IsSupported(string path) => AllExtensions.Contains(Path.GetExtension(path).ToLowerInvariant());

    /// <summary>A lowercase identifier made from the file name, safe for asset paths.</summary>
    private static readonly string[] GenericStems = { "scene", "model", "untitled", "export", "mesh", "object", "default", "main", "weapon", "gun", "asset", "file", "source" };

    public static string SafeName(string path)
    {
        var raw = Path.GetFileNameWithoutExtension(path).ToLowerInvariant();
        // "scene.gltf" in "fps_ak-74m_animations/" is better named after its folder.
        if (GenericStems.Contains(raw) && Path.GetFileName(Path.GetDirectoryName(path) ?? "") is { Length: > 0 } folder)
            raw = folder.ToLowerInvariant();
        var chars = raw.Select(c => char.IsLetterOrDigit(c) ? c : '_').ToArray();
        var name = new string(chars).Trim('_');
        while (name.Contains("__"))
            name = name.Replace("__", "_");
        if (name.Length == 0)
            name = "weapon";
        if (char.IsDigit(name[0]))
            name = "w_" + name;
        return name.Length > 48 ? name[..48] : name;
    }

    public static WeaponAsset Load(string path)
    {
        if (!File.Exists(path))
            throw new WeaponImportException($"File not found: {path}");
        byte[] data;
        try
        {
            data = File.ReadAllBytes(path);
        }
        catch (Exception e)
        {
            throw new WeaponImportException($"Could not read {Path.GetFileName(path)}: {e.Message}", e);
        }
        var dir = Path.GetDirectoryName(path) ?? "";
        return Load(data, path, uri => File.ReadAllBytes(Path.Combine(dir, Uri.UnescapeDataString(uri))));
    }

    public static WeaponAsset Load(byte[] data, string path, Func<string, byte[]>? externalBuffers = null)
    {
        var ext = Path.GetExtension(path).ToLowerInvariant();
        var name = SafeName(path);
        if (data.Length == 0)
            throw new WeaponImportException($"{Path.GetFileName(path)} is empty.");
        try
        {
            var asset = ext switch
            {
                ".fbx" => FbxWeaponReader.Read(data, name, path),
                ".glb" or ".gltf" => GltfWeaponReader.Read(data, name, path, externalBuffers),
                _ => throw new WeaponImportException($"{ext} files are not supported. Use FBX, GLB, glTF or a VMDL."),
            };
            return MaterialFallbacks.Apply(asset, path);
        }
        catch (WeaponImportException)
        {
            throw;
        }
        catch (Exception e) when (e is FormatException or EndOfStreamException or IndexOutOfRangeException or ArgumentException or InvalidOperationException or KeyNotFoundException or System.Text.Json.JsonException or NotSupportedException or ArithmeticException)
        {
            throw new WeaponImportException($"{Path.GetFileName(path)} could not be read: {e.Message}", e);
        }
    }
}