using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; using static PKHeX.Core.BatchEditingUtil; namespace PKHeX.Core; /// /// Base logic for editing entities with user provided list. /// /// /// Caches reflection results for the provided types, and provides utility methods for fetching properties and applying instructions. /// public abstract class BatchEditingBase : IBatchEditor where TObject : notnull { private readonly Type[] _types; private readonly string[] _customProperties; private readonly Dictionary.AlternateLookup>[] _props; private readonly Lazy _properties; protected BatchEditingBase(Type[] types, string[] customProperties, int expectedMax) { _types = types; _customProperties = customProperties; _props = GetPropertyDictionaries(types, expectedMax); _properties = new Lazy(() => GetPropArray(_props, customProperties)); } /// /// Property names, indexed by . /// public string[][] Properties => _properties.Value; /// /// Gets the list of supported entity types. /// public IReadOnlyList Types => _types; protected abstract TMeta CreateMeta(TObject entity); protected abstract bool ShouldModify(TObject entity); protected abstract bool TryHandleSetOperation(StringInstruction cmd, TMeta info, TObject entity, out ModifyResult result); protected abstract bool TryHandleFilter(StringInstruction cmd, TMeta info, TObject entity, out bool isMatch); /// /// Tries to fetch the entity property from the cache of available properties. /// public bool TryGetHasProperty(TObject entity, ReadOnlySpan name, [NotNullWhen(true)] out PropertyInfo? pi) => TryGetHasProperty(entity.GetType(), name, out pi); /// /// Tries to fetch the entity property from the cache of available properties. /// public bool TryGetHasProperty(Type type, ReadOnlySpan name, [NotNullWhen(true)] out PropertyInfo? pi) { var index = _types.IndexOf(type); if (index < 0) { pi = null; return false; } var localProps = _props[index]; return localProps.TryGetValue(name, out pi); } /// /// Gets a list of entity types that implement the requested property. /// public IEnumerable GetTypesImplementing(string property) { for (int i = 0; i < _types.Length; i++) { var type = _types[i]; var localProps = _props[i]; if (!localProps.TryGetValue(property, out var pi)) continue; yield return $"{type.Name}: {pi.PropertyType.Name}"; } } /// /// Gets the type of the entity property using the saved cache of properties. /// public bool TryGetPropertyType(string propertyName, [NotNullWhen(true)] out string? result, int typeIndex = 0) { if (_customProperties.Contains(propertyName)) { result = "Custom"; return true; } result = null; if (typeIndex == 0) { foreach (var p in _props) { if (!p.TryGetValue(propertyName, out var pi)) continue; result = pi.PropertyType.Name; return true; } return false; } int index = typeIndex - 1; if ((uint)index >= _props.Length) index = 0; var pr = _props[index]; if (!pr.TryGetValue(propertyName, out var info)) return false; result = info.PropertyType.Name; return true; } /// /// Checks if the entity is filtered by the provided filters. /// public bool IsFilterMatch(IEnumerable filters, TObject entity) { var info = CreateMeta(entity); var localProps = GetProps(entity); foreach (var filter in filters) { if (!IsFilterMatch(filter, info, entity, localProps)) return false; } return true; } /// /// Tries to modify the entity. /// public bool TryModifyIsSuccess(TObject entity, IEnumerable filters, IEnumerable modifications, Func? modifier = null) => TryModify(entity, filters, modifications, modifier) is ModifyResult.Modified; /// /// Tries to modify the entity using instructions and a custom modifier delegate. /// public ModifyResult TryModify(TObject entity, IEnumerable filters, IEnumerable modifications, Func? modifier = null) { if (!ShouldModify(entity)) return ModifyResult.Skipped; var info = CreateMeta(entity); var localProps = GetProps(entity); foreach (var cmd in filters) { try { if (!IsFilterMatch(cmd, info, entity, localProps)) return ModifyResult.Filtered; } catch (Exception ex) { Debug.WriteLine(ex.Message); return ModifyResult.Error; } } var error = false; var result = ModifyResult.Skipped; if (modifier is { } func) { try { if (!func(entity)) return ModifyResult.Skipped; result = ModifyResult.Modified; } catch (Exception ex) { Debug.WriteLine(ex.Message); return ModifyResult.Error; } } foreach (var cmd in modifications) { try { var tmp = SetProperty(cmd, entity, info, localProps); if (tmp == ModifyResult.Error) error = true; else if (tmp != ModifyResult.Skipped) result = tmp; } catch (Exception ex) { Debug.WriteLine(ex.Message); error = true; } } if (error) result |= ModifyResult.Error; return result; } private static Dictionary.AlternateLookup>[] GetPropertyDictionaries(ReadOnlySpan types, int expectedMax) { var result = new Dictionary.AlternateLookup>[types.Length]; for (int i = 0; i < types.Length; i++) result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic, expectedMax).GetAlternateLookup>(); return result; } private static Dictionary GetPropertyDictionary(Type type, Func> selector, int expectedMax) { var dict = new Dictionary(expectedMax); var localProps = selector(type); foreach (var p in localProps) dict.TryAdd(p.Name, p); return dict; } private static string[][] GetPropArray(Dictionary.AlternateLookup>[] types, ReadOnlySpan extra) { var result = new string[types.Length + 2][]; var p = result.AsSpan(1, types.Length); for (int i = 0; i < p.Length; i++) { var type = types[i].Dictionary; string[] combine = [..type.Keys, ..extra]; combine.Sort(); p[i] = combine; } var first = p[0]; var any = new HashSet(first); var all = new HashSet(first); foreach (var set in p[1..]) { any.UnionWith(set); all.IntersectWith(set); } var arrAny = any.ToArray(); arrAny.Sort(); result[0] = arrAny; var arrAll = all.ToArray(); arrAll.Sort(); result[^1] = arrAll; return result; } private Dictionary.AlternateLookup> GetProps(TObject entity) { var type = entity.GetType(); var typeIndex = _types.IndexOf(type); return _props[typeIndex]; } private bool IsFilterMatch(StringInstruction cmd, TMeta info, TObject entity, Dictionary.AlternateLookup> localProps) { if (TryHandleFilter(cmd, info, entity, out var isMatch)) return isMatch; return IsPropertyFiltered(cmd, entity, localProps); } private static bool IsPropertyFiltered(StringInstruction cmd, TObject entity, Dictionary.AlternateLookup> localProps) { if (!localProps.TryGetValue(cmd.PropertyName, out var pi)) return false; if (!pi.CanRead) return false; var val = cmd.PropertyValue; if (val.StartsWith(PointerToken) && localProps.TryGetValue(val.AsSpan(1), out var opi)) { var result = opi.GetValue(entity) ?? throw new NullReferenceException(); return cmd.Comparer.IsCompareOperator(pi.CompareTo(entity, result)); } return cmd.Comparer.IsCompareOperator(pi.CompareTo(entity, val)); } private ModifyResult SetProperty(StringInstruction cmd, TObject entity, TMeta info, Dictionary.AlternateLookup> localProps) { if (cmd.Operation == InstructionOperation.Set && TryHandleSetOperation(cmd, info, entity, out var result)) return result; if (!localProps.TryGetValue(cmd.PropertyName, out var pi)) return ModifyResult.Error; if (!pi.CanWrite) return ModifyResult.Error; if (cmd.Operation != InstructionOperation.Set) return ApplyNumericOperation(entity, cmd, pi, localProps); if (!TryResolveOperandValue(cmd, entity, localProps, out var value)) return ModifyResult.Error; ReflectUtil.SetValue(pi, entity, value); return ModifyResult.Modified; } private static ModifyResult ApplyNumericOperation(TObject entity, StringInstruction cmd, PropertyInfo pi, Dictionary.AlternateLookup> localProps) { if (!pi.CanRead) return ModifyResult.Error; if (!TryGetNumericType(pi.PropertyType, out var numericType)) return ModifyResult.Error; var currentValue = pi.GetValue(entity); if (currentValue is null) return ModifyResult.Error; if (!TryResolveOperandValue(cmd, entity, localProps, out var operandValue)) return ModifyResult.Error; if (!TryApplyNumericOperation(numericType, cmd.Operation, currentValue, operandValue, out var value)) return ModifyResult.Error; ReflectUtil.SetValue(pi, entity, value); return ModifyResult.Modified; } private static bool TryResolveOperandValue(StringInstruction cmd, TObject entity, Dictionary.AlternateLookup> localProps, [NotNullWhen(true)] out object? value) { if (cmd.Random) { value = cmd.RandomValue; return true; } var propertyValue = cmd.PropertyValue; if (propertyValue.StartsWith(PointerToken) && localProps.TryGetValue(propertyValue.AsSpan(1), out var opi)) { value = opi.GetValue(entity); return value is not null; } value = propertyValue; return true; } private static bool TryGetNumericType(Type type, out Type numericType) { numericType = Nullable.GetUnderlyingType(type) ?? type; // bool isNullable = type != numericType; return numericType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(INumber<>)); } private static bool TryApplyNumericOperation(Type numericType, InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out object? result) { result = null; if (numericType == typeof(byte)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(sbyte)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(short)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(ushort)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(int)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(uint)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(long)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(ulong)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(nint)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(nuint)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(BigInteger)) return ApplyBinaryInteger(currentValue, operandValue, operation, out result); if (numericType == typeof(float)) return ApplyNumeric(currentValue, operandValue, operation, out result); if (numericType == typeof(double)) return ApplyNumeric(currentValue, operandValue, operation, out result); if (numericType == typeof(decimal)) return ApplyNumeric(currentValue, operandValue, operation, out result); return false; } private static bool ApplyNumeric(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result) where T : INumber { if (operation.IsBitwise) { result = null; return false; } var success = TryApplyNumericOperationCore(operation, currentValue, operandValue, out var typed); result = typed; return success; } private static bool ApplyBinaryInteger(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result) where T : IBinaryInteger { var success = operation.IsBitwise ? TryApplyBinaryIntegerOperationCore(operation, currentValue, operandValue, out var typed) : TryApplyNumericOperationCore(operation, currentValue, operandValue, out typed); result = typed; return success; } private static bool TryApplyNumericOperationCore(InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out T? result) where T : INumber { if (!TryConvertNumeric(currentValue, out var left) || !TryConvertNumeric(operandValue, out var right)) { result = default; return false; } return TryApplyNumericOperationCore(operation, left, right, out result); } private static bool TryApplyNumericOperationCore(InstructionOperation operation, T left, T right, [NotNullWhen(true)] out T? result) where T : INumber { try { result = operation switch { InstructionOperation.Add => left + right, InstructionOperation.Subtract => left - right, InstructionOperation.Multiply => left * right, InstructionOperation.Divide => left / right, InstructionOperation.Modulo => left % right, _ => right, }; return true; } catch (DivideByZeroException) { result = default; return false; } } private static bool TryApplyBinaryIntegerOperationCore(InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out T? result) where T : IBinaryInteger { if (!TryConvertNumeric(currentValue, out var left) || !TryConvertNumeric(operandValue, out var right)) { result = default; return false; } return TryApplyBinaryIntegerOperationCore(operation, left, right, out result); } private static bool TryApplyBinaryIntegerOperationCore(InstructionOperation operation, T left, T right, [NotNullWhen(true)] out T? result) where T : IBinaryInteger { try { switch (operation) { case InstructionOperation.BitwiseAnd: result = left & right; return true; case InstructionOperation.BitwiseOr: result = left | right; return true; case InstructionOperation.BitwiseXor: result = left ^ right; return true; case InstructionOperation.BitwiseShiftLeft: result = left << int.CreateChecked(right); return true; case InstructionOperation.BitwiseShiftRight: result = left >> int.CreateChecked(right); return true; default: result = default; return false; } } catch (OverflowException) { result = default; return false; } } private static bool TryConvertNumeric(object value, [NotNullWhen(true)] out T? result) where T : INumber { if (value is T typed) { result = typed; return true; } if (value is string text) { if (T.TryParse(text, CultureInfo.InvariantCulture, out var parsed)) { result = parsed; return true; } result = default; return false; } if (value is IConvertible) { try { var converted = Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); if (converted is T convertedValue) { result = convertedValue; return true; } } catch (Exception ex) { Debug.WriteLine(ex.Message); } } result = default; return false; } }