Editor/WeaponImporter/Core/Analysis/NameTokens.cs
#nullable enable annotations

using System.Text;

namespace WeaponImporter.Core.Analysis;

/// <summary>
/// Splits bone, mesh and animation names into lowercase tokens: <c>"tag_MagazineLeft01"</c>
/// becomes <c>tag, magazine, left, 01</c>. Rig noise such as <c>mixamorig:</c> or
/// <c>Armature|</c> prefixes is stripped first.
/// </summary>
public static class NameTokens
{
    private static readonly string[] Prefixes = { "mixamorig", "armature", "rootnode", "scene", "valvebiped" };

    public static string[] Split(string? name)
    {
        if (string.IsNullOrWhiteSpace(name))
            return Array.Empty<string>();

        // Keep the part after namespace/take separators ("Armature|reload", "rig:mag").
        var clean = name.Trim();
        var sep = clean.LastIndexOfAny(new[] { '|', ':' });
        if (sep >= 0 && sep < clean.Length - 1)
            clean = clean[(sep + 1)..];

        var tokens = new List<string>();
        var sb = new StringBuilder();
        void Flush()
        {
            if (sb.Length > 0)
                tokens.Add(sb.ToString().ToLowerInvariant());
            sb.Clear();
        }

        for (var i = 0; i < clean.Length; i++)
        {
            var c = clean[i];
            if (!char.IsLetterOrDigit(c))
            {
                Flush();
                continue;
            }
            if (sb.Length > 0)
            {
                var prev = clean[i - 1];
                var lowerToUpper = char.IsLower(prev) && char.IsUpper(c);
                var letterDigit = char.IsLetter(prev) != char.IsLetter(c);
                // "ADSFire" -> ads, fire: an upper run followed by upper+lower splits before the last upper.
                var acronymEnd = char.IsUpper(prev) && char.IsUpper(c) && i + 1 < clean.Length && char.IsLower(clean[i + 1]);
                if (lowerToUpper || letterDigit || acronymEnd)
                    Flush();
            }
            sb.Append(c);
        }
        Flush();

        return tokens.Where(t => !Prefixes.Contains(t)).ToArray();
    }

    /// <summary>
    /// Whether any token equals one of the aliases, or starts with an alias of five or more
    /// letters ("sights" matches "sight"; "handle" does not match "hand").
    /// </summary>
    public static bool Has(IReadOnlyList<string> tokens, params string[] aliases)
    {
        foreach (var t in tokens)
            foreach (var a in aliases)
                if (t == a || (a.Length >= 5 && t.StartsWith(a, StringComparison.Ordinal)))
                    return true;
        return false;
    }

    /// <summary>Concatenated tokens, for aliases spanning tokens ("fireempty", "reloadempty").</summary>
    public static string Joined(IReadOnlyList<string> tokens) => string.Concat(tokens);
}