// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
// ReSharper disable StaticMemberInGenericType
namespace WallstopStudios.UnityHelpers.Core.Extension
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using Attributes;
using Helper;
///
/// Internal cache data structure for storing enum name mappings optimized for fast lookup.
///
internal sealed class EnumNameCacheData
{
public readonly string[] namesArray;
public readonly ConcurrentDictionary namesDict;
public readonly bool useArray;
public readonly ulong minValue;
public readonly int arrayLength;
public EnumNameCacheData(
string[] namesArray,
ConcurrentDictionary namesDict,
bool useArray,
ulong minValue,
int arrayLength
)
{
this.namesArray = namesArray;
this.namesDict = namesDict;
this.useArray = useArray;
this.minValue = minValue;
this.arrayLength = arrayLength;
}
}
///
/// Provides high-performance cached enum name lookups with zero allocation for frequently accessed enum values.
///
/// The unmanaged enum type to cache names for.
///
/// Uses array-based lookup for enums with small ranges (≤256 values) and dictionary-based lookup for larger enums.
/// Thread-safe with reader-writer locking for dictionary operations.
/// Performance: O(1) lookups for both array and dictionary strategies.
///
public static class EnumNameCache
where T : unmanaged, Enum
{
// Use instance holder to avoid static field access overhead on Mono
private static readonly EnumNameCacheData Cache;
static EnumNameCache()
{
Array rawValues = Enum.GetValues(typeof(T));
T[] values = Unsafe.As(ref rawValues);
string[] names = Enum.GetNames(typeof(T));
// Try to determine if we can use array-based lookup
ulong minVal = ulong.MaxValue;
ulong maxVal = 0;
bool hasValidRange = true;
for (int i = 0; i < values.Length; i++)
{
if (!EnumNumericHelper.TryConvertToUInt64(values[i], out ulong val))
{
hasValidRange = false;
break;
}
if (val < minVal)
{
minVal = val;
}
if (val > maxVal)
{
maxVal = val;
}
}
// Use array if the range is reasonable (< 256 elements)
ulong range = hasValidRange && maxVal >= minVal ? maxVal - minVal + 1 : 0;
bool useArray = hasValidRange && range is <= 256 and > 0;
string[] namesArray;
ConcurrentDictionary namesDict;
ulong minValue;
int arrayLength;
if (useArray)
{
minValue = minVal;
arrayLength = (int)range;
namesArray = new string[arrayLength];
for (int i = 0; i < values.Length; i++)
{
T value = values[i];
if (EnumNumericHelper.TryConvertToUInt64(value, out ulong key))
{
int index = (int)(key - minValue);
if (index >= 0 && index < arrayLength)
{
string name = names[i];
if (namesArray[index] == null)
{
namesArray[index] = name;
}
}
}
}
namesDict = new ConcurrentDictionary();
}
else
{
// Fall back to dictionary
namesDict = new ConcurrentDictionary();
for (int i = 0; i < values.Length; i++)
{
T value = values[i];
if (!EnumNumericHelper.TryConvertToUInt64(value, out ulong key))
{
continue;
}
string name = value.ToString("G");
namesDict.TryAdd(key, name);
}
namesArray = null;
minValue = 0;
arrayLength = 0;
}
Cache = new EnumNameCacheData(namesArray, namesDict, useArray, minValue, arrayLength);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ToCachedName(T value)
{
if (!EnumNumericHelper.TryConvertToUInt64(value, out ulong key))
{
return value.ToString("G");
}
EnumNameCacheData cache = Cache;
if (cache.useArray && cache.namesArray != null)
{
ulong index = key - cache.minValue;
if (index < (ulong)cache.arrayLength)
{
string existing = cache.namesArray[index];
if (existing != null)
{
return existing;
}
string generated = value.ToString("G");
string prior = Interlocked.CompareExchange(
ref cache.namesArray[index],
generated,
null
);
return prior ?? generated;
}
}
ConcurrentDictionary namesDict = cache.namesDict;
if (namesDict != null)
{
if (namesDict.TryGetValue(key, out string name))
{
return name;
}
return namesDict.GetOrAdd(key, enumValue => enumValue.ToString("G"));
}
return value.ToString("G");
}
}
///
/// Internal cache data structure for storing enum display name mappings from EnumDisplayNameAttribute.
///
internal sealed class EnumDisplayNameCacheData
{
public readonly string[] namesArray;
public readonly ConcurrentDictionary namesDict;
public readonly bool useArray;
public readonly ulong minValue;
public readonly int arrayLength;
public EnumDisplayNameCacheData(
string[] namesArray,
ConcurrentDictionary namesDict,
bool useArray,
ulong minValue,
int arrayLength
)
{
this.namesArray = namesArray;
this.namesDict = namesDict;
this.useArray = useArray;
this.minValue = minValue;
this.arrayLength = arrayLength;
}
}
///
/// Provides high-performance cached enum display name lookups using EnumDisplayNameAttribute values.
///
/// The unmanaged enum type to cache display names for.
///
/// Uses reflection to extract EnumDisplayNameAttribute values at startup, then caches for fast access.
/// Falls back to field name if attribute is not present.
/// Uses array-based lookup for enums with small ranges (≤256 values) and dictionary-based lookup for larger enums.
/// Thread-safe with concurrent dictionary operations.
/// Performance: O(1) lookups for both array and dictionary strategies.
///
public static class EnumDisplayNameCache
where T : unmanaged, Enum
{
// Use instance holder to avoid static field access overhead on Mono
private static readonly EnumDisplayNameCacheData Cache;
static EnumDisplayNameCache()
{
Type type = typeof(T);
FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
// First pass: determine range
ulong minVal = ulong.MaxValue;
ulong maxVal = 0;
bool hasValidRange = true;
for (int i = 0; i < fields.Length; i++)
{
T value = (T)fields[i].GetValue(null);
if (!EnumNumericHelper.TryConvertToUInt64(value, out ulong val))
{
hasValidRange = false;
break;
}
if (val < minVal)
{
minVal = val;
}
if (val > maxVal)
{
maxVal = val;
}
}
// Use array if the range is reasonable (< 256 elements)
ulong range = hasValidRange && maxVal >= minVal ? maxVal - minVal + 1 : 0;
bool useArray = hasValidRange && range is <= 256 and > 0;
string[] namesArray;
ConcurrentDictionary namesDict;
ulong minValue;
int arrayLength;
if (useArray)
{
minValue = minVal;
arrayLength = (int)range;
namesArray = new string[arrayLength];
for (int i = 0; i < fields.Length; i++)
{
FieldInfo field = fields[i];
string name = field.IsAttributeDefined(
out EnumDisplayNameAttribute displayName,
inherit: false
)
? displayName.DisplayName
: field.Name;
T value = (T)field.GetValue(null);
if (EnumNumericHelper.TryConvertToUInt64(value, out ulong key))
{
int index = (int)(key - minValue);
if (index >= 0 && index < arrayLength)
{
namesArray[index] = name;
}
}
}
namesDict = new ConcurrentDictionary(
Environment.ProcessorCount,
fields.Length
);
}
else
{
// Fall back to dictionary
namesDict = new ConcurrentDictionary(
Environment.ProcessorCount,
fields.Length
);
for (int i = 0; i < fields.Length; i++)
{
FieldInfo field = fields[i];
string name = field.IsAttributeDefined(
out EnumDisplayNameAttribute displayName,
inherit: false
)
? displayName.DisplayName
: field.Name;
T value = (T)field.GetValue(null);
if (!EnumNumericHelper.TryConvertToUInt64(value, out ulong key))
{
continue;
}
namesDict.TryAdd(key, name);
}
namesArray = null;
minValue = 0;
arrayLength = 0;
}
Cache = new EnumDisplayNameCacheData(
namesArray,
namesDict,
useArray,
minValue,
arrayLength
);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ToDisplayName(T value)
{
if (!EnumNumericHelper.TryConvertToUInt64(value, out ulong key))
{
return value.ToString("G");
}
EnumDisplayNameCacheData cache = Cache;
if (cache.useArray && cache.namesArray != null)
{
ulong index = key - cache.minValue;
if (index < (ulong)cache.arrayLength)
{
string existing = cache.namesArray[index];
if (existing != null)
{
return existing;
}
string generated = value.ToString("G");
string prior = Interlocked.CompareExchange(
ref cache.namesArray[index],
generated,
null
);
return prior ?? generated;
}
}
ConcurrentDictionary namesDict = cache.namesDict;
if (namesDict != null)
{
if (namesDict.TryGetValue(key, out string name))
{
return name;
}
return namesDict.GetOrAdd(key, enumValue => enumValue.ToString("G"));
}
return value.ToString("G");
}
}
///
/// Extension methods for enum types providing allocation-free flag checking and cached name conversions.
///
///
/// Thread Safety: All methods are thread-safe.
/// Performance: Methods use caching and aggressive inlining for optimal performance.
///
public static class EnumExtensions
{
///
/// Checks if an enum value has a specific flag set without boxing allocation.
///
/// The unmanaged enum type (must be a flags enum for meaningful results).
/// The enum value to check.
/// The flag to check for.
/// True if the flag is set, false otherwise.
///
/// Null handling: N/A - operates on value types.
/// Thread-safe: Yes.
/// Performance: O(1) - uses bitwise operations on underlying numeric type.
/// Allocations: Zero allocations (no boxing). Falls back to built-in HasFlag for unsupported enum sizes.
/// Edge cases: Works with enum sizes 1, 2, 4, or 8 bytes. Larger sizes fall back to HasFlag.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasFlagNoAlloc(this T value, T flag)
where T : unmanaged, Enum
{
if (
!EnumNumericHelper.TryConvertToUInt64(value, out ulong valueUnderlying)
|| !EnumNumericHelper.TryConvertToUInt64(flag, out ulong flagUnderlying)
)
{
// Fallback for unsupported enum sizes
return value.HasFlag(flag);
}
return (valueUnderlying & flagUnderlying) == flagUnderlying;
}
///
/// Converts an enum value to its display name using the EnumDisplayNameAttribute if present, otherwise the field name.
///
/// The unmanaged enum type.
/// The enum value to convert.
/// The display name string from the attribute, or the enum's ToString() if not cached.
///
/// Null handling: N/A - operates on value types.
/// Thread-safe: Yes.
/// Performance: O(1) - uses cached lookups via EnumDisplayNameCache.
/// Allocations: Zero for cached values, one string allocation for uncached values on first access.
/// Edge cases: Returns ToString("G") for values not in the cache.
///
public static string ToDisplayName(this T value)
where T : unmanaged, Enum
{
return EnumDisplayNameCache.ToDisplayName(value);
}
///
/// Converts a collection of enum values to their display names.
///
/// The unmanaged enum type.
/// The collection of enum values to convert.
/// An enumerable of display name strings.
///
/// Null handling: Throws if enumerable is null when enumerated.
/// Thread-safe: Yes for reads.
/// Performance: O(n) where n is the number of enum values. Uses cached lookups.
/// Allocations: Allocates LINQ iterator. Minimal allocations for cached display names.
/// Edge cases: Empty collection returns empty enumerable.
/// Laziness: Uses deferred execution - values are transformed only when enumerated.
///
public static IEnumerable ToDisplayNames(this IEnumerable enumerable)
where T : unmanaged, Enum
{
return enumerable.Select(value => value.ToDisplayName());
}
///
/// Converts an enum value to its name string using a high-performance cache.
///
/// The unmanaged enum type.
/// The enum value to convert.
/// The cached name string, or ToString("G") if not cached.
///
/// Null handling: N/A - operates on value types.
/// Thread-safe: Yes with reader-writer locking.
/// Performance: O(1) - uses cached lookups via EnumNameCache.
/// Allocations: Zero for cached values, one string allocation for uncached values on first access.
/// Edge cases: Returns ToString("G") for values not in the cache.
///
public static string ToCachedName(this T value)
where T : unmanaged, Enum
{
return EnumNameCache.ToCachedName(value);
}
///
/// Converts a collection of enum values to their cached name strings.
///
/// The unmanaged enum type.
/// The collection of enum values to convert.
/// An enumerable of cached name strings.
///
/// Null handling: Throws if enumerable is null when enumerated.
/// Thread-safe: Yes for reads.
/// Performance: O(n) where n is the number of enum values. Uses cached lookups.
/// Allocations: Allocates LINQ iterator. Minimal allocations for cached names.
/// Edge cases: Empty collection returns empty enumerable.
/// Laziness: Uses deferred execution - values are transformed only when enumerated.
///
public static IEnumerable ToCachedNames(this IEnumerable enumerable)
where T : unmanaged, Enum
{
return enumerable.Select(value => value.ToCachedName());
}
}
///
/// Internal helper class for converting enum values to their underlying numeric representation without boxing.
///
/// The unmanaged enum type.
internal static class EnumNumericHelper
where T : unmanaged, Enum
{
private static readonly int Size = Unsafe.SizeOf();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryConvertToUInt64(T value, out ulong result)
{
ref T valueRef = ref Unsafe.AsRef(in value);
switch (Size)
{
case 1:
result = Unsafe.As(ref valueRef);
return true;
case 2:
result = Unsafe.As(ref valueRef);
return true;
case 4:
result = Unsafe.As(ref valueRef);
return true;
case 8:
result = Unsafe.As(ref valueRef);
return true;
default:
result = default;
return false;
}
}
}
}