// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Editor.CustomDrawers.Utils
{
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using WallstopStudios.UnityHelpers.Core.Attributes;
///
/// Provides shared condition evaluation logic for WShowIf attribute drawers.
///
///
/// This utility class consolidates common code used by both the standard PropertyDrawer
/// and the Odin Inspector drawer implementations of WShowIf. By centralizing these
/// elements, we ensure consistent behavior and eliminate code duplication.
///
public static class ShowIfConditionEvaluator
{
///
/// Binding flags for resolving members on types.
///
public const BindingFlags MemberBindingFlags =
BindingFlags.Instance
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.FlattenHierarchy;
private static readonly Dictionary CompareToMethodCache = new();
///
/// Tries to evaluate the condition and determine whether the property should be shown.
///
/// The current value of the condition field.
/// The WShowIf attribute containing comparison settings.
///
/// When this method returns true, contains whether the property should be shown.
///
///
/// True if the condition was successfully evaluated; false if evaluation failed.
///
public static bool TryEvaluateCondition(
object conditionValue,
WShowIfAttribute showIf,
out bool shouldShow
)
{
bool? evaluation = EvaluateCondition(conditionValue, showIf);
if (!evaluation.HasValue)
{
shouldShow = true;
return false;
}
bool matched = evaluation.Value;
shouldShow = showIf.inverse ? !matched : matched;
return true;
}
///
/// Evaluates the condition based on the condition value and attribute settings.
///
/// The current value of the condition field.
/// The WShowIf attribute containing comparison settings.
///
/// True if condition matches, false if it doesn't match, null if evaluation failed.
///
public static bool? EvaluateCondition(object conditionValue, WShowIfAttribute attribute)
{
WShowIfComparison comparison = attribute.comparison;
#pragma warning disable CS0618 // Type or member is obsolete
if (comparison == WShowIfComparison.Unknown)
#pragma warning restore CS0618 // Type or member is obsolete
{
comparison = WShowIfComparison.Equal;
}
switch (comparison)
{
case WShowIfComparison.IsNull:
return IsNull(conditionValue);
case WShowIfComparison.IsNotNull:
return !IsNull(conditionValue);
case WShowIfComparison.IsNullOrEmpty:
return IsNullOrEmpty(conditionValue);
case WShowIfComparison.IsNotNullOrEmpty:
return !IsNullOrEmpty(conditionValue);
default:
break;
}
object[] expectedValues = attribute.expectedValues;
if (conditionValue is bool boolean)
{
return EvaluateBooleanCondition(boolean, comparison, expectedValues);
}
if (expectedValues == null || expectedValues.Length == 0)
{
return null;
}
switch (comparison)
{
case WShowIfComparison.Equal:
return MatchesAny(conditionValue, expectedValues);
case WShowIfComparison.NotEqual:
return !MatchesAny(conditionValue, expectedValues);
case WShowIfComparison.GreaterThan:
case WShowIfComparison.GreaterThanOrEqual:
case WShowIfComparison.LessThan:
case WShowIfComparison.LessThanOrEqual:
object referenceValue = expectedValues[0];
return EvaluateRelationalComparison(conditionValue, referenceValue, comparison);
default:
return MatchesAny(conditionValue, expectedValues);
}
}
///
/// Evaluates a boolean condition against expected values.
///
/// The boolean value to evaluate.
/// The comparison type.
/// The expected values to compare against.
/// True if condition matches, false otherwise.
public static bool? EvaluateBooleanCondition(
bool value,
WShowIfComparison comparison,
object[] expectedValues
)
{
if (expectedValues is { Length: > 0 })
{
bool matches = MatchesAny(value, expectedValues);
if (comparison == WShowIfComparison.NotEqual)
{
return !matches;
}
return matches;
}
if (comparison == WShowIfComparison.NotEqual)
{
return !value;
}
return value;
}
///
/// Checks if the condition value matches any of the expected values.
///
/// The current value to check.
/// The expected values to compare against.
/// True if condition value matches any expected value.
public static bool MatchesAny(object conditionValue, object[] expectedValues)
{
for (int index = 0; index < expectedValues.Length; index++)
{
if (ValuesEqual(conditionValue, expectedValues[index]))
{
return true;
}
}
return false;
}
///
/// Compares two values for equality, handling enums, flags, and numeric conversions.
///
/// The actual value.
/// The expected value.
/// True if values are considered equal.
public static bool ValuesEqual(object actual, object expected)
{
if (ReferenceEquals(actual, expected))
{
return true;
}
if (actual == null || expected == null)
{
return false;
}
if (actual.Equals(expected))
{
return true;
}
Type actualType = actual.GetType();
Type expectedType = expected.GetType();
try
{
if (actualType.IsEnum || expectedType.IsEnum)
{
long actualValue = Convert.ToInt64(actual);
long expectedValue = Convert.ToInt64(expected);
Type enumType = actualType.IsEnum ? actualType : expectedType;
if (enumType.IsDefined(typeof(FlagsAttribute), false))
{
return (actualValue & expectedValue) == expectedValue;
}
return actualValue == expectedValue;
}
}
catch
{
return false;
}
if (actual is not IConvertible || expected is not IConvertible)
{
return false;
}
try
{
double actualValue = Convert.ToDouble(actual);
double expectedValue = Convert.ToDouble(expected);
return Math.Abs(actualValue - expectedValue) < double.Epsilon;
}
catch
{
return false;
}
}
///
/// Evaluates a relational comparison between two values.
///
/// The actual value.
/// The expected value to compare against.
/// The comparison type.
///
/// True if comparison succeeds, false if it fails, null if comparison is not possible.
///
public static bool? EvaluateRelationalComparison(
object actual,
object expected,
WShowIfComparison comparison
)
{
if (!TryCompare(actual, expected, out int compareResult))
{
return null;
}
switch (comparison)
{
case WShowIfComparison.GreaterThan:
return compareResult > 0;
case WShowIfComparison.GreaterThanOrEqual:
return compareResult >= 0;
case WShowIfComparison.LessThan:
return compareResult < 0;
case WShowIfComparison.LessThanOrEqual:
return compareResult <= 0;
default:
return null;
}
}
///
/// Tries to compare two values and returns the comparison result.
///
/// The actual value.
/// The expected value.
///
/// When this method returns true, contains the comparison result
/// (negative if actual less than expected, zero if equal, positive if greater than).
///
/// True if comparison was successful; false otherwise.
public static bool TryCompare(object actual, object expected, out int comparisonResult)
{
comparisonResult = 0;
if (actual == null || expected == null)
{
return false;
}
IComparable comparable = actual as IComparable;
if (comparable != null)
{
object converted = ConvertValue(
actual.GetType(),
expected,
out bool conversionSucceeded
);
if (conversionSucceeded)
{
try
{
comparisonResult = comparable.CompareTo(converted);
return true;
}
catch
{
// Fall through to other comparison methods
}
}
}
if (TryGenericComparableCompare(actual, expected, out comparisonResult, false))
{
return true;
}
IComparable expectedComparable = expected as IComparable;
if (expectedComparable != null)
{
object converted = ConvertValue(
expected.GetType(),
actual,
out bool conversionSucceeded
);
if (conversionSucceeded)
{
try
{
comparisonResult = -expectedComparable.CompareTo(converted);
return true;
}
catch
{
// Fall through to other comparison methods
}
}
}
if (TryGenericComparableCompare(expected, actual, out comparisonResult, true))
{
return true;
}
if (
TryConvertToDouble(actual, out double actualDouble)
&& TryConvertToDouble(expected, out double expectedDouble)
)
{
comparisonResult = actualDouble.CompareTo(expectedDouble);
return true;
}
return false;
}
///
/// Converts a value to the target type.
///
/// The type to convert to.
/// The value to convert.
/// True if conversion succeeded; false otherwise.
/// The converted value, or null if conversion failed.
public static object ConvertValue(Type targetType, object value, out bool success)
{
success = true;
if (value == null)
{
if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null)
{
success = false;
}
return null;
}
if (targetType.IsInstanceOfType(value))
{
return value;
}
try
{
if (targetType.IsEnum)
{
Type underlyingType = Enum.GetUnderlyingType(targetType);
object numericValue = Convert.ChangeType(value, underlyingType);
return Enum.ToObject(targetType, numericValue);
}
return Convert.ChangeType(value, targetType);
}
catch
{
success = false;
return null;
}
}
///
/// Tries to convert a value to a double.
///
/// The value to convert.
/// When this method returns true, contains the converted double.
/// True if conversion succeeded; false otherwise.
public static bool TryConvertToDouble(object value, out double result)
{
result = 0d;
if (value == null)
{
return false;
}
try
{
result = Convert.ToDouble(value);
return true;
}
catch
{
return false;
}
}
///
/// Tries to compare values using IComparable<T> interface.
///
/// The left-hand side value.
/// The right-hand side value.
///
/// When this method returns true, contains the comparison result.
///
///
/// If true, the comparison result is inverted (for when comparing rhs to lhs).
///
/// True if comparison was successful; false otherwise.
public static bool TryGenericComparableCompare(
object lhs,
object rhs,
out int comparisonResult,
bool invert
)
{
comparisonResult = 0;
if (lhs == null)
{
return false;
}
Type lhsType = lhs.GetType();
if (!CompareToMethodCache.TryGetValue(lhsType, out MethodInfo compareTo))
{
compareTo = FindCompareToMethod(lhsType);
CompareToMethodCache[lhsType] = compareTo;
}
if (compareTo == null)
{
return false;
}
Type genericArgument = compareTo.GetParameters()[0].ParameterType;
object converted = ConvertValue(genericArgument, rhs, out bool success);
if (!success)
{
return false;
}
try
{
object[] args = new object[1];
args[0] = converted;
object compareResult = compareTo.Invoke(lhs, args);
comparisonResult = Convert.ToInt32(compareResult);
if (invert)
{
comparisonResult = -comparisonResult;
}
return true;
}
catch
{
return false;
}
}
///
/// Finds the CompareTo method for IComparable<T> on a type.
///
/// The type to search for CompareTo method.
/// The CompareTo method, or null if not found.
public static MethodInfo FindCompareToMethod(Type type)
{
Type[] interfaces = type.GetInterfaces();
for (int index = 0; index < interfaces.Length; index++)
{
Type iface = interfaces[index];
if (
!iface.IsGenericType
|| iface.GetGenericTypeDefinition() != typeof(IComparable<>)
)
{
continue;
}
Type genericArgument = iface.GetGenericArguments()[0];
Type[] paramTypes = new Type[1];
paramTypes[0] = genericArgument;
MethodInfo method = iface.GetMethod("CompareTo", paramTypes);
if (method != null)
{
return method;
}
}
return null;
}
///
/// Checks if a value is null, with special handling for Unity objects.
///
/// The value to check.
/// True if value is null or a destroyed Unity object.
public static bool IsNull(object value)
{
if (value == null)
{
return true;
}
// Use ReferenceEquals to check if the cast succeeded, avoiding Unity's
// overloaded == operator which returns true for destroyed objects.
// We want to detect destroyed objects here, not skip them.
UnityEngine.Object unityObject = value as UnityEngine.Object;
if (!ReferenceEquals(unityObject, null))
{
// Unity's == operator returns true for destroyed objects
return unityObject == null;
}
return false;
}
///
/// Checks if a value is null or empty (for strings, collections, and enumerables).
///
/// The value to check.
/// True if value is null, empty string, or empty collection.
public static bool IsNullOrEmpty(object value)
{
if (IsNull(value))
{
return true;
}
string stringValue = value as string;
if (stringValue != null)
{
return stringValue.Length == 0;
}
ICollection collection = value as ICollection;
if (collection != null)
{
return collection.Count == 0;
}
IEnumerable enumerable = value as IEnumerable;
if (enumerable != null)
{
IEnumerator enumerator = enumerable.GetEnumerator();
try
{
return !enumerator.MoveNext();
}
finally
{
IDisposable disposable = enumerator as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
}
return false;
}
}
#endif
}