using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Xml.Serialization;
namespace OpenTap
{
///
/// This attribute is used to dynamically embed the properties of an object into another object.
///
///
/// A property of type PT declared on a type DT decorated with this attribute will not be visible in reflection information (ITypeData) for DT.
/// Instead all properties declared on PT will be visible on DT as though they had been declared there.
///
[AttributeUsage(AttributeTargets.Property)]
public class EmbedPropertiesAttribute : Attribute
{
///
/// When true, property name of the owning property is used as prefix for embedded properties. E.g., the name will be 'EmbeddedProperty.X'.
/// A prefix can help prevent name-clashing issues if multiple properties gets the same name.
///
public bool PrefixPropertyName { get; set; } = true;
///
/// Custom prefix for embedded properties. This will overwrite PrefixPropertyName.
/// A prefix can help prevent name-clashing issues if multiple properties gets the same name.
///
public string Prefix { get; set; } = null;
}
//
// The code below implements the embedded member data dynamic reflection.
// It creates a wrapper around existing types and gives them an extra layer
// which contains the added properties.
//
class EmbeddedMemberData : IMemberData, IDynamicMemberData, IEmbeddedMemberData
{
public IMemberData OwnerMember => ownerMember;
public IMemberData InnerMember => innerMember;
readonly IMemberData ownerMember;
readonly IMemberData innerMember;
private readonly IEnumerable additionalAttributes;
public ITypeData DeclaringType => ownerMember.DeclaringType;
public ITypeData TypeDescriptor => innerMember.TypeDescriptor;
public bool Writable => innerMember.Writable;
public bool Readable => innerMember.Readable;
public IEnumerable Attributes => attributes ?? (attributes = loadAndTransformAttributes());
public string Name { get; }
public object GetValue(object owner)
{
var target = ownerMember.GetValue(owner);
if (target == null) return null;
return innerMember.GetValue(target);
}
public void SetValue(object owner, object value)
{
var target = ownerMember.GetValue(owner);
if (target == null) throw new NullReferenceException($"Embedded property object owner ('{ownerMember.Name}') is null.");
innerMember.SetValue(target, value);
}
public EmbeddedMemberData(IMemberData ownerMember, IMemberData innerMember, IEnumerable additionalAttributes)
{
this.ownerMember = ownerMember;
this.innerMember = innerMember;
this.additionalAttributes = additionalAttributes;
var embed = ownerMember.GetAttribute();
if (embed.PrefixPropertyName)
{
var prefix_name = embed.Prefix ?? ownerMember.Name;
Name = prefix_name + "." + this.innerMember.Name;
}
else
Name = this.innerMember.Name;
}
public override string ToString() => $"EmbMem:{Name}";
object[] attributes;
///
/// Loads and transform the list of attributes.
/// Some attributes are sensitive to naming, the known ones are AvailableValuesAttribute,
/// SuggestedValueAttribute, and EnabledIf. Others might exist, but they are, for now, not supported.
/// When NoPrefix is set on EmbedProperties, then there is no issue, but with the prefix, those names also needs
/// to be transformed.
/// Additionally, there is some special behavior wanted for Display, for usability and to avoid name clashing:
/// - If a Display name/group is set on the owner property, that should turn into a group for the embedded properties.
/// - If there is no display name, use the prefix name for the group.
/// - If there is no prefix name, don't touch DisplayAttribute.
///
object[] loadAndTransformAttributes()
{
string prefix_name = null;
var list = innerMember.Attributes.ToList();
var embed = ownerMember.GetAttribute();
if (embed.PrefixPropertyName)
prefix_name = embed.Prefix ?? ownerMember.Name;
string[] pre_group1;
string pre_name;
double ownerOrder = -10000;
bool collapsed = false;
{
var owner_display = ownerMember.GetAttribute();
if (owner_display == null)
{
owner_display = new DisplayAttribute(embed.PrefixPropertyName ? ownerMember.Name : "");
pre_group1 = owner_display.Group;
pre_name = owner_display.Name;
}
else
{
collapsed = owner_display.Collapsed;
ownerOrder = owner_display.Order;
pre_group1 = owner_display.Group;
pre_name = owner_display.Name;
}
}
string name;
string[] post_group;
var d = list.OfType().FirstOrDefault();
if (d != null)
{
list.Remove(d); // need to re-add a new DisplayAttribute.
name = d.Name;
post_group = d.Group;
if (d.Collapsed)
collapsed = true;
}
else
{
name = Name;
post_group = Array.Empty();
}
string[] prefixGroups;
if (embed.PrefixPropertyName == false && string.IsNullOrWhiteSpace(pre_name))
prefixGroups = Array.Empty();
else
prefixGroups = new[] {pre_name};
var groups = pre_group1.Concat(prefixGroups).Concat(post_group).ToArray();
double order = -10000;
{
// calculate order:
// if owner order is set, add it to the display order.
if (d != null && d.Order != -10000.0)
{
order = d.Order;
if (ownerOrder != -10000.0)
{
// if an order is specified, add the two orders together.
// this makes sure that property groups can be arranged.
order += ownerOrder;
}
}
else
{
order = ownerOrder;
}
}
d = new DisplayAttribute(name, Groups: groups,
Description: d?.Description, Order: order, Collapsed: collapsed);
list.Add(d);
if (prefix_name != null)
{
// Transform properties that has issues with embedding.
for (var i = 0; i < list.Count; i++)
{
var item = list[i];
if (item is AvailableValuesAttribute avail)
{
list[i] = new AvailableValuesAttribute(prefix_name + "." + avail.PropertyName);
}
else if (item is SuggestedValuesAttribute sug)
{
list[i] = new SuggestedValuesAttribute(prefix_name + "." + sug.PropertyName);
}
else if (item is EnabledIfAttribute enabled)
{
list[i] = new EnabledIfAttribute(prefix_name + "." + enabled.PropertyName, enabled.Values)
{HideIfDisabled = enabled.HideIfDisabled};
}
}
}
return attributes = list.Concat(additionalAttributes ?? []).ToArray();
}
bool IDynamicMemberData.IsDisposed => OwnerMember is IDynamicMemberData dyn && dyn.IsDisposed;
}
class EmbeddedTypeData : ITypeData
{
IMemberData[] listedEmbeddedMembers;
public ITypeData BaseType { get; set; }
public bool CanCreateInstance => BaseType.CanCreateInstance;
public IEnumerable Attributes => BaseType.Attributes;
public string Name => EmbeddedTypeDataProvider.exp + BaseType.Name;
public object CreateInstance(object[] arguments) => BaseType.CreateInstance(arguments);
public IMemberData GetMember(string name) => GetMembers().FirstOrDefault(x => x.Name == name);
public IEnumerable GetMembers() => BaseType.GetMembers()
.Where(x => x.HasAttribute() == false)
.Concat(listedEmbeddedMembers ??= ListEmbeddedMembers());
public override int GetHashCode() => BaseType.GetHashCode() * 86966513 + typeof(EmbeddedTypeData).GetHashCode();
public override bool Equals(object obj)
{
if(obj is EmbeddedTypeData e)
return e.BaseType == BaseType;
return false;
}
[ThreadStatic]
static HashSet currentlyListing;
public IEnumerable GetEmbeddingMembers()
{
foreach (var member in BaseType.GetMembers())
{
if (member.HasAttribute())
{
yield return member;
}
}
}
internal IMemberData[] ListEmbeddedMembers()
{
if (currentlyListing == null)
currentlyListing = new HashSet();
List embeddedMembers = new List();
if (currentlyListing.Contains(this))
return embeddedMembers.ToArray();
currentlyListing.Add(this);
foreach (var member in BaseType.GetMembers())
{
if (member.HasAttribute())
{
var xmlIgnore = member.HasAttribute();
var members = member.TypeDescriptor.GetMembers();
foreach(var m in members)
{
if (m.HasAttribute())
{
if (xmlIgnore == false)
xmlIgnore = m.HasAttribute();
members = EmbeddedTypeDataProvider.FromTypeData(member.TypeDescriptor).GetMembers();
break;
}
}
object[] additionalAttributes = xmlIgnore ? [new XmlIgnoreAttribute()] : [];
foreach (var innermember in members)
embeddedMembers.Add(new EmbeddedMemberData(member, innermember, additionalAttributes));
}
}
currentlyListing.Remove(this);
return embeddedMembers.ToArray();
}
public override string ToString() => $"EmbType:{Name}";
}
class EmbeddedTypeDataProvider : IStackedTypeDataProvider
{
public double Priority => 10.5;
internal const string exp = "emb:";
static ConditionalWeakTable internedValues = new ConditionalWeakTable();
static EmbeddedTypeData getOrCreate(ITypeData e) => internedValues.GetValue(e, t => t.GetMembers().Any(m => m.HasAttribute()) ? new EmbeddedTypeData{BaseType = e} : null);
public static EmbeddedTypeData FromTypeData(ITypeData baseType) => getOrCreate(baseType);
public ITypeData GetTypeData(string identifier, TypeDataProviderStack stack)
{
if (identifier.StartsWith(exp))
{
var tp = TypeData.GetTypeData(identifier.Substring(exp.Length));
if (tp != null)
{
var embTp = FromTypeData(tp);
if (embTp != null)
return embTp;
return tp;
}
}
return null;
}
class KeyBox
{
public int Key { get; set; }
}
// this key table is used to keep track of the members of dynamic types.
// if the key has changed, then it means that the cache should be invalidated.
static ConditionalWeakTable keyTable = new ConditionalWeakTable();
public ITypeData GetTypeData(object obj, TypeDataProviderStack stack)
{
var typeData = stack.GetTypeData(obj);
var nowKey = DynamicMember.GetTypeDataKey(obj);
bool reload = false;
if (nowKey != 0)
{
if (keyTable.TryGetValue(obj, out var currentKey))
{
if (currentKey.Key != nowKey)
{
reload = true;
currentKey.Key = nowKey;
}
}
else
{
keyTable.Add(obj, new KeyBox(){Key = nowKey});
reload = true;
}
}
if (reload)
{
internedValues.Remove(typeData);
}
if (internedValues.TryGetValue(typeData, out EmbeddedTypeData val))
{
return val ?? typeData;
}
if (typeData.GetMembers().Any(x => x.HasAttribute()))
return FromTypeData(typeData);
// assign null to the value.
return internedValues.GetValue(typeData, t => null) ?? typeData;
}
}
}