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