using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Humanizer; using Newtonsoft.Json; namespace Artemis.Core.Modules { /// /// Represents a data model that contains information on a game/application etc. /// public abstract class DataModel { private readonly HashSet _activePathsHashSet = new(); private readonly List _activePaths = new(); private readonly Dictionary _dynamicChildren = new(); /// /// Creates a new instance of the class /// protected DataModel() { // These are both set right after construction to keep the constructor of inherited classes clean Module = null!; DataModelDescription = null!; ActivePaths = new(_activePaths); DynamicChildren = new(_dynamicChildren); } /// /// Gets the module this data model belongs to /// [JsonIgnore] [DataModelIgnore] public Module Module { get; internal set; } /// /// Gets the describing this data model /// [JsonIgnore] [DataModelIgnore] public DataModelPropertyAttribute DataModelDescription { get; internal set; } /// /// Gets the is expansion status indicating whether this data model expands the main data model /// [DataModelIgnore] public bool IsExpansion { get; internal set; } /// /// Gets an read-only dictionary of all dynamic children /// [DataModelIgnore] public ReadOnlyDictionary DynamicChildren { get; } /// /// Gets a read-only list of s targeting this data model /// [DataModelIgnore] public ReadOnlyCollection ActivePaths { get; } /// /// Returns a read-only collection of all properties in this datamodel that are to be ignored /// /// public ReadOnlyCollection GetHiddenProperties() { return Module.HiddenProperties; } /// /// Gets the property description of the provided property info /// /// If found, the property description attribute, otherwise . public virtual DataModelPropertyAttribute? GetPropertyDescription(PropertyInfo propertyInfo) { return (DataModelPropertyAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(DataModelPropertyAttribute)); } #region Dynamic children /// /// Adds a dynamic child to this data model /// /// The key of the child, must be unique to this data model /// The initial value of the dynamic child /// The resulting dynamic child which can be used to further update the value public DynamicChild AddDynamicChild(string key, T initialValue) { return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute()); } /// /// Adds a dynamic child to this data model /// /// The key of the child, must be unique to this data model /// The initial value of the dynamic child /// A human readable name for your dynamic child, shown in the UI /// An optional description, shown in the UI /// The resulting dynamic child which can be used to further update the value public DynamicChild AddDynamicChild(string key, T initialValue, string name, string? description = null) { return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute {Name = name, Description = description}); } /// /// Adds a dynamic child to this data model /// /// The key of the child, must be unique to this data model /// The initial value of the dynamic child /// A data model property attribute describing the dynamic child /// The resulting dynamic child which can be used to further update the value public DynamicChild AddDynamicChild(string key, T initialValue, DataModelPropertyAttribute attribute) { if (key == null) throw new ArgumentNullException(nameof(key)); if (initialValue == null) throw new ArgumentNullException(nameof(initialValue)); if (attribute == null) throw new ArgumentNullException(nameof(attribute)); if (key.Contains('.')) throw new ArtemisCoreException("The provided key contains an illegal character (.)"); if (_dynamicChildren.ContainsKey(key)) { throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + "because the key is already in use on by another dynamic property this data model."); } if (GetType().GetProperty(key) != null) { throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + "because the key is already in use by a static property on this data model."); } // Make sure a name is on the attribute or funny things might happen attribute.Name ??= key.Humanize(); if (initialValue is DataModel dynamicDataModel) { dynamicDataModel.Module = Module; dynamicDataModel.DataModelDescription = attribute; } DynamicChild dynamicChild = new(initialValue, key, attribute); _dynamicChildren.Add(key, dynamicChild); OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key)); return dynamicChild; } /// /// Gets a previously added dynamic child by its key /// /// The key of the dynamic child public DynamicChild GetDynamicChild(string key) { if (key == null) throw new ArgumentNullException(nameof(key)); return DynamicChildren[key]; } /// /// Gets a previously added dynamic child by its key /// /// The typer of dynamic child you are expecting /// The key of the dynamic child /// public DynamicChild GetDynamicChild(string key) { if (key == null) throw new ArgumentNullException(nameof(key)); return (DynamicChild) DynamicChildren[key]; } /// /// Gets a previously added dynamic child by its key /// /// The key of the dynamic child /// /// When this method returns, the associated with the specified key, /// if the key is found; otherwise, . This parameter is passed uninitialized. /// /// /// if the data model contains the dynamic child; otherwise /// public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild) { if (key == null) throw new ArgumentNullException(nameof(key)); dynamicChild = null; if (!DynamicChildren.TryGetValue(key, out DynamicChild? value)) return false; dynamicChild = value; return true; } /// /// Gets a previously added dynamic child by its key /// /// The typer of dynamic child you are expecting /// The key of the dynamic child /// /// When this method returns, the associated with the specified /// key, if the key is found and the type matches; otherwise, . This parameter is passed /// uninitialized. /// /// /// if the data model contains the dynamic child; otherwise /// public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild) { if (key == null) throw new ArgumentNullException(nameof(key)); dynamicChild = null; if (!DynamicChildren.TryGetValue(key, out DynamicChild? value)) return false; if (value is not DynamicChild typedDynamicChild) return false; dynamicChild = typedDynamicChild; return true; } /// /// Removes a dynamic child from the data model by its key /// /// The key of the dynamic child to remove public void RemoveDynamicChildByKey(string key) { if (key == null) throw new ArgumentNullException(nameof(key)); if (!_dynamicChildren.TryGetValue(key, out DynamicChild? dynamicChild)) return; _dynamicChildren.Remove(key); OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key)); } /// /// Removes a dynamic child from this data model /// /// The dynamic data child to remove public void RemoveDynamicChild(DynamicChild dynamicChild) { if (dynamicChild == null) throw new ArgumentNullException(nameof(dynamicChild)); List keys = _dynamicChildren.Where(kvp => kvp.Value.BaseValue == dynamicChild).Select(kvp => kvp.Key).ToList(); foreach (string key in keys) { _dynamicChildren.Remove(key); OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key)); } } /// /// Removes all dynamic children from this data model /// public void ClearDynamicChildren() { while (_dynamicChildren.Any()) RemoveDynamicChildByKey(_dynamicChildren.First().Key); } // Used a runtime by data model paths only internal T? GetDynamicChildValue(string key) { if (TryGetDynamicChild(key, out DynamicChild? dynamicChild) && dynamicChild.BaseValue != null) return (T) dynamicChild.BaseValue; return default; } #endregion #region Paths /// /// Determines whether the provided dot-separated path is in use /// /// The path to check per example: MyDataModelChild.MyDataModelProperty /// /// If any child of the given path will return true as well; if /// only an exact path match returns . /// internal bool IsPropertyInUse(string path, bool includeChildren) { path = path.ToUpperInvariant(); return includeChildren ? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal)) : _activePathsHashSet.Contains(path); } internal void AddDataModelPath(DataModelPath path) { if (_activePaths.Contains(path)) return; _activePaths.Add(path); // Add to the hashset if this is the first path pointing string hashPath = path.Path.ToUpperInvariant(); if (!_activePathsHashSet.Contains(hashPath)) _activePathsHashSet.Add(hashPath); OnActivePathAdded(new DataModelPathEventArgs(path)); } internal void RemoveDataModelPath(DataModelPath path) { if (!_activePaths.Remove(path)) return; // Remove from the hashset if this was the last path pointing there if (_activePaths.All(p => p.Path != path.Path)) _activePathsHashSet.Remove(path.Path.ToUpperInvariant()); OnActivePathRemoved(new DataModelPathEventArgs(path)); } #endregion #region Events /// /// Occurs when a dynamic child has been added to this data model /// public event EventHandler? DynamicChildAdded; /// /// Occurs when a dynamic child has been removed from this data model /// public event EventHandler? DynamicChildRemoved; /// /// Occurs when a dynamic child has been added to this data model /// public event EventHandler? ActivePathAdded; /// /// Occurs when a dynamic child has been removed from this data model /// public event EventHandler? ActivePathRemoved; /// /// Invokes the event /// protected virtual void OnDynamicDataModelAdded(DynamicDataModelChildEventArgs e) { DynamicChildAdded?.Invoke(this, e); } /// /// Invokes the event /// protected virtual void OnDynamicDataModelRemoved(DynamicDataModelChildEventArgs e) { DynamicChildRemoved?.Invoke(this, e); } /// /// Invokes the event /// protected virtual void OnActivePathAdded(DataModelPathEventArgs e) { ActivePathAdded?.Invoke(this, e); } /// /// Invokes the event /// protected virtual void OnActivePathRemoved(DataModelPathEventArgs e) { ActivePathRemoved?.Invoke(this, e); } #endregion } }