using axXez.Utils; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Reflection; namespace axXez.TS3.Protocol { /// Base class for all TeamSpeak text-protocol property objects. Handles field binding via and tracks which properties changed on each Apply call. public abstract class TsEntity { internal TsDataEntry AppliedData { get; set; } internal TsEntityChangeInfo LastChanges { get; set; } protected TsEntity(TsDataEntry data) => Apply(data); static readonly ConcurrentDictionary>>> _applyActions = new(); internal TsEntityChangeInfo Apply(TsEntity entity) { if (entity.AppliedData == null) return new TsEntityChangeInfo(new HashSet()); return Apply(entity.AppliedData); } internal TsEntityChangeInfo Apply(TsDataEntry incoming) { var changed = new HashSet(); lock (this) { foreach (var apply in _applyActions.GetOrAdd(GetType(), BuildActions)) apply(this, incoming, changed); if (AppliedData == null) AppliedData = incoming; else AppliedData.Merge(incoming); LastChanges = new TsEntityChangeInfo(changed); } return LastChanges; } internal void Update(string key, string value) { if (AppliedData == null) return; lock (this) { if (!AppliedData.Inject(key, value)) return; //find propery if exists and update foreach (var prop in GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var attr = prop.GetCustomAttribute(); if (attr == null) continue; if (!attr.Keys.Contains(key)) continue; var val = Converter(prop.PropertyType)(value); if (val == null) return; prop.SetValue(this, val); return; } } } static List>> BuildActions(Type type) { var result = new List>>(); foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var attr = prop.GetCustomAttribute(); if (attr == null) continue; var name = prop.Name; if (prop.PropertyType.IsAssignableTo(typeof(TsEntity))) { result.Add((entity, incoming, changed) => { var existing = (TsEntity)prop.GetValue(entity); if (existing == null) { prop.SetValue(entity, incoming.GetEntity(prop.PropertyType)); changed.Add(name); } else if (existing.Apply(incoming).Any) { changed.Add(name); } }); continue; } var keys = attr.Keys; var convert = Converter(prop.PropertyType); result.Add((entity, incoming, changed) => { if (!incoming.TryGetValue(out var raw, keys)) return; var val = convert(raw); if (val == null) return; if (!Equals(prop.GetValue(entity), val)) { changed.Add(name); prop.SetValue(entity, val); } }); } return result; } static Func Converter(Type type) => type switch { _ when type == typeof(string) => s => s, _ when type == typeof(bool) => s => TsDataEntry.TryParseBool(s, out var v) ? (object)v : null, _ when type == typeof(TimeSpan) => s => TsDataEntry.TryParseTimespan(s, out var v) ? v : null, _ when type == typeof(DateTime) => s => TsDataEntry.TryParseDatetime(s, out var v) ? v : null, _ when IsNumeric(type) => CompileNumeric(type), _ when StringConverter.HasConverter(type) => s => StringConverter.TryConvert(s, type, out var v) ? v : null, _ => throw new NotSupportedException($"No converter for type {type.Name}") }; static bool IsNumeric(Type type) => type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(INumber<>)); static Func CompileNumeric(Type type) { var method = typeof(TsEntity) .GetMethod(nameof(TryParseNumeric), BindingFlags.Static | BindingFlags.NonPublic)! .MakeGenericMethod(type); return (Func)Delegate.CreateDelegate(typeof(Func), method); } static object? TryParseNumeric(string input) where T : INumber => TsDataEntry.TryParseNumber(input, out T value) ? (object)value : null; } }