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;
}
}