using Qrakhen.Qamp.Core.Collections; using Qrakhen.Qamp.Core.Logging; using Qrakhen.Qamp.Core.Values.Objects; using System.Linq.Expressions; using System.Reflection; namespace Qrakhen.Qamp.Core.Values; public delegate Value ExtensionDelegate(Value target, Value[] parameters); public class ExtensionException(string message, object? context = null) : QampException(message, context); public abstract class ExtensionAttribute : Attribute; public abstract class TypedExtensionAttribute : ExtensionAttribute { public ValueType ValueType { get; set; } = ValueType.Undefined; public Type? ValueObjectType { get; set; } = null; public string? ValueObjectName { get; set; } public bool Nullable { get; set; } = false; public TypeInfo TypeInfo => new TypeInfo(ValueType, ValueObjectType, ValueObjectName, Nullable); } [AttributeUsage(AttributeTargets.Method)] public class ExtensionMethodAttribute : ExtensionAttribute { public string? Name { get; set; } = null; } [AttributeUsage(AttributeTargets.Method)] public class ReturnsAttribute : TypedExtensionAttribute; [AttributeUsage(AttributeTargets.Parameter)] public class SelfAttribute : TypedExtensionAttribute; [AttributeUsage(AttributeTargets.Class)] public class ExtensionClassAttribute : SelfAttribute { public string? Name { get; set; } = null; } [AttributeUsage(AttributeTargets.Parameter)] public class ParameterAttribute : TypedExtensionAttribute { public string? Name { get; set; } = null; public bool Optional { get; set; } = false; } public class ExtensionMethod( TypeInfo targetType, TypeInfo returnType, string name, ExtensionDelegate callback, string[] parameters) { private static readonly Register> _register = []; private static readonly ILogger _logger = LoggerService.Get(); public readonly string Name = name; public readonly TypeInfo TargetType = targetType; public readonly TypeInfo ReturnType = returnType; public readonly ExtensionDelegate Callback = callback; public readonly string[] Parameters = parameters.ToArray(); public readonly string Key = parameters.Length > 0 ? $"{name}_{parameters.Length}" : name; public static ExtensionMethod? Get(TypeInfo targetType, string name, int parameters = 0) { if (parameters > 0) name = $"{name}_{parameters}"; if (!_register.Has(targetType)) return null; if (!_register[targetType].Has(name)) return null; return _register[targetType][name]; } public static void Register(IEnumerable extensions) { foreach (var extension in extensions) Register(extension); } public static void Register(ExtensionMethod extension) { if (!_register.Has(extension.TargetType)) _register.Add(extension.TargetType, []); if (_register[extension.TargetType].Has(extension.Key)) throw new ExtensionException($"Extension {extension.Key} already exists for type {extension.TargetType}."); _register[extension.TargetType].Add(extension.Key, extension); } public static void Register(TypeInfo targetType, TypeInfo returnType, string name, ExtensionDelegate callback, params string[] parameters) => Register(new ExtensionMethod(targetType, returnType, name, callback, parameters)); private static ExtensionMethod RenderExtension(MethodInfo method) { _logger.Verbose($"Rendering extension from {method}..."); var attr = method.GetCustomAttribute(); if (attr is null) throw new ExtensionException($"Expected an [ExtensionMethod] attribute on method {method} for extension compilation.", method); var parameters = method.GetParameters(); if (parameters.Length < 1) throw new ExtensionException($"Tried to define method {method} as extension, but extensions require their first parameter to be , the target value of the extension.", method); var selfAttr = parameters[0].GetCustomAttribute() ?? method.DeclaringType?.GetCustomAttribute(); if (selfAttr is null) throw new ExtensionException($"Expected first parameter of {method} to have a [Self], or its declaring type to have a [ExtensionClass] attribute in order to define the extension's target type."); TypeInfo anyType = TypeInfo.Any; string name = attr.Name ?? method.Name; TypeInfo selfType = selfAttr.TypeInfo; TypeInfo returnType = method.GetCustomAttribute()?.TypeInfo ?? anyType; // todo: assume returned type from actual return value string[] parameterNames = parameters.Subset(1).Select(p => p.Name!).ToArray() ?? []; // todo: use the actual parameter attribute for this. ExtensionDelegate callback = RenderDelegate(method); return new ExtensionMethod(selfType, returnType, name, callback, parameterNames); } private static ExtensionDelegate RenderDelegate(MethodInfo method) { _logger.Verbose($"Rendering delegate expression..."); var selfParameter = Expression.Parameter(typeof(Value), "self"); var argsParameter = Expression.Parameter(typeof(Value[]), "args"); ParameterInfo[] parameterInfos = method.GetParameters(); var parameterExpressions = new Expression[parameterInfos.Length - 1]; for (int i = 0; i < parameterInfos.Length; i++) { ConstantExpression index = Expression.Constant(i); BinaryExpression indexExpression = Expression.ArrayIndex(argsParameter, index); /* todo: unsure Type requiredType = parameterInfos[i].ParameterType; if (indexExpression.Type != requiredType) { parameterExpressions[i] = Expression.Convert(indexExpression, requiredType); }*/ parameterExpressions[i] = indexExpression; } Expression[] arguments = [selfParameter, .. parameterExpressions]; MethodCallExpression callExpression = Expression.Call(method, arguments); Expression final; if (method.ReturnType != typeof(Value)) { ConstructorInfo? ctor = typeof(Value).GetConstructor([ method.ReturnType ]); if (ctor is null) throw new ExtensionException($"Can not compile extension from method {method}, its return type '{method.ReturnType}' is not accepted by any Value constructors.", method); final = Expression.New(ctor, callExpression); } else { final = callExpression; } LambdaExpression lambda = Expression.Lambda(final, selfParameter, argsParameter); _logger.Verbose($"Done: ", lambda); return (ExtensionDelegate)lambda.Compile(); } public static IEnumerable CompileExtensionsFromType(Type type) { _logger.Debug($"Compiling static extension methods from {type}..."); List extensions = []; var methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public) .Where(method => method.GetCustomAttribute() is not null) .ToArray(); if (methods.Length == 0) { _logger.Warn($"Tried to compile static extension methods from {type}, but no public static methods were found declared inside it."); return extensions; } foreach (var method in methods) { extensions.Add(RenderExtension(method)); } return extensions; } static ExtensionMethod() { Register(CompileExtensionsFromType(typeof(StringExtensions))); } public Value Call(Value self, Value[] parameters) { return Callback(self, parameters); } public override string ToString() { return $"{TargetType}.{Name}({string.Join(", ", Parameters)}) (NativeExtension)"; } } [ExtensionClass(ValueType = ValueType.String)] public static class StringExtensions { [ExtensionMethod] public static long Length(Objects.String self) { return self.Value?.Length ?? 0; } [ExtensionMethod] public static long IndexOf(Objects.String self, Objects.String needle) { if (needle.Value is null) return -1; return self.Value?.IndexOf(needle.Value) ?? -1; } [ExtensionMethod] public static Objects.Array Split(Objects.String self, Objects.String splitter) { string? _splitter = splitter.Value; string? _string = self.Value; if (_string is null) return new Objects.Array([]); if (_splitter is null) return new Objects.Array([ Obj.Create(self) ]); string[] parts = _string.Split(_splitter); Value[] values = new Value[parts.Length]; for (int i = 0; i < parts.Length; i++) values[i] = Objects.String.Make(parts[i]); return new Objects.Array(values); } }