using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; using Upsilon.BaseTypes; using Upsilon.Binder; using Upsilon.Binder.VariableSymbols; using Upsilon.StandardLibraries; using Type = Upsilon.BaseTypes.Type; namespace Upsilon.BoundTypes { public class UserDataBoundTypeDefinition : BoundTypeDefinition { public string Name { get; protected set; } public Dictionary Properties { get; protected set; } internal UserDataBoundTypeDefinition(System.Type backingType, string name) : base(new TypeContainer(name), backingType) { Name = backingType.Name; ValidInternalTypes = ValidInternalTypes.Concat(GetParentTypes(backingType)).ToArray(); } private static IEnumerable GetParentTypes(System.Type type) { // is there any base type? if (type == null) { yield break; } // return all implemented or inherited interfaces foreach (var i in type.GetInterfaces()) { yield return i; } // return all inherited types var currentBaseType = type.BaseType; while (currentBaseType != null) { // We don't want the global object type to be returned if (currentBaseType == typeof(object)) break; yield return currentBaseType; currentBaseType = currentBaseType.BaseType; } } public UserDataBoundTypeDefinition(string name, Dictionary backingType) : base(new TypeContainer(name), new System.Type[0]) { Name = name; Properties = backingType; } public static UserDataBoundTypeDefinition Create(System.Type backingType, string name) { var obj = new UserDataBoundTypeDefinition(backingType, name) { Properties = new Dictionary(), Name = name }; const BindingFlags generalBindAttributes = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; var fields = backingType.GetFields(generalBindAttributes).Select(x => new UserDataBoundProperty() { Name = x.Name, ActualType = x.FieldType.Name, Type = x.FieldType.GetScriptType(), }); foreach (var f in fields) { obj.Properties.Add(f.Name.ToLowerInvariant(), f); } PropertyInfo[] realProperties; if (backingType.IsInterface) { realProperties = new[]{backingType} .Concat(backingType.GetInterfaces()) .SelectMany(x => x.GetProperties(generalBindAttributes)).ToArray(); } else { realProperties = backingType.GetProperties(generalBindAttributes); } var properties = realProperties.Select(x => new UserDataBoundProperty() { Name = x.Name, ActualType = x.PropertyType.Name, Type = x.PropertyType.GetScriptType(), }); foreach (var f in properties) { var lowerPropertyName = f.Name.ToLowerInvariant(); if (!obj.Properties.ContainsKey(lowerPropertyName)) obj.Properties.Add(lowerPropertyName, f); } var methods = new Dictionary(); var backingMethods = backingType.GetMethods(generalBindAttributes); foreach (var backingMethod in backingMethods) { if (backingMethod.IsSpecialName) continue; var methodName = backingMethod.Name; var passScriptReference = false; var attribute = backingMethod.GetCustomAttribute(typeof(ScriptFunctionAttribute)); if (attribute is ScriptFunctionAttribute sfa ) { methodName = sfa.Name; passScriptReference = sfa.PassScriptReference; } var parameters = backingMethod.GetParameters().Select(parameter => new UserDataBoundFunctionParameter() { Name = parameter.Name, ActualType = parameter.ParameterType.Name, Type = StaticScope.DeriveValidTypes(parameter.ParameterType), IsOptional = parameter.IsOptional }).ToArray(); var option = new UserDataBoundMethodOption(backingMethod.ReturnType.GetScriptType(), parameters, passScriptReference); if (methods.TryGetValue(methodName, out var func)) { func.Options.Add(option); } else { methods.Add(methodName, new UserDataBoundMethod(methodName, new List() {option}) { Type = Type.Function }); } } foreach (var f in methods) { var cleanedName = f.Value.Name.ToLowerInvariant(); // TODO: handle this better, considering overloads if (obj.Properties.ContainsKey(cleanedName)) continue; obj.Properties.Add(cleanedName, f.Value); } return obj; } } public class UserDataBoundEnumDefinition : UserDataBoundTypeDefinition { public UserDataBoundEnumDefinition(System.Type enumType, string name) : base(enumType, name) { if (!enumType.IsEnum) throw new Exception("Trying to bind an enum with a type that's not an enum"); Properties = new Dictionary(); var enumValues = Enum.GetValues(enumType); Name = name; for (var i=0; i < enumValues.Length; i++) { var value = enumValues.GetValue(i); var valueName = value.ToString().ToLowerInvariant(); Properties.Add(valueName, new UserDataBoundProperty() { Name = valueName, ActualType = enumType.ToString(), Type = new TypeContainer(name) }); } } public UserDataBoundEnumDefinition(IEnumerable values, string name) : base(name, new Dictionary()) { Properties = new Dictionary(); Name = name; foreach (var value in values) { var valueName = value.ToLowerInvariant(); Properties.Add(valueName, new UserDataBoundProperty() { Name = valueName, Type = new TypeContainer(name), ActualType = name }); } } } public class UserDataBoundProperty { public string Name { get; set; } public TypeContainer Type { get; set; } public virtual string ActualType { get; set; } public string Comment { get; set; } } public class UserDataBoundFunctionParameter : UserDataBoundProperty { public bool IsOptional { get; set; } } public class UserDataBoundMethod: UserDataBoundProperty { public override string ActualType => "Function"; public List Options; public UserDataBoundMethod(string name, List options) { Name = name; Options = options; } public UserDataBoundMethodOption Validate(ImmutableArray callingParameters) { return Options.FirstOrDefault(x => x.ValidateParameters(callingParameters)); } } public class UserDataBoundMethodOption { public UserDataBoundMethodOption(TypeContainer resultType, UserDataBoundFunctionParameter[] parameters, bool passScriptReference) { ResultType = resultType; Parameters = parameters; PassScriptReference = passScriptReference; NonOptionalParameterCount = Parameters.Count(x => !x.IsOptional); } public TypeContainer ResultType { get; } public UserDataBoundFunctionParameter[] Parameters { get; } public bool PassScriptReference { get; } private int NonOptionalParameterCount { get; } public bool ValidateParameters(ImmutableArray callingParameters) { var effectiveLength = callingParameters.Length; if (PassScriptReference) effectiveLength++; if (effectiveLength < NonOptionalParameterCount || effectiveLength > Parameters.Length) { return false; } var callingIndexOffset = 0; for (var i = 0; i < callingParameters.Length; i++) { var functionParameter = Parameters[i]; if (i == 0 && PassScriptReference) { callingIndexOffset++; continue; } var callingParameter = callingParameters[i + callingIndexOffset]; if (callingParameter.ValueType == Type.Unknown || callingParameter.ValueType == Type.Nil || functionParameter.Type == Type.Unknown) continue; if (!functionParameter.Type.Type.HasFlag(callingParameter.ValueType)) { return false; } if (functionParameter.Type.Type.HasFlag(Type.UserData)) { var variable = Binder.Binder.ResolveVariable(callingParameter, null) as UserDataVariableSymbol; var parent = (UserDataBoundTypeDefinition) variable?.BoundTypeDefinition; if (variable != null && variable.TypeContainer == Type.UserData) { if (parent.ValidInternalTypes.Length != 0) { if (functionParameter.ActualType != null) { if (!parent.ValidInternalTypes.Any(x => string.Equals(x.Name, functionParameter.ActualType))) { return false; } } else if (!string.Equals(parent.Name, functionParameter.ActualType, StringComparison.InvariantCultureIgnoreCase)) { return false; } } } } } return true; } } }