using Comal.Classes; using Expressive; using InABox.Core; using InABox.Database; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace PRS.Shared.Events; public interface IEventData { IEvent Event { get; } IEnumerable ReferencedVariables { get; } } public class EventData : IEventData where T : IEvent where TDataModel : IEventDataModel { public T Event { get; set; } /// /// A list of triggers for this event. If any of the triggers match, the event runs. /// public List> Triggers { get; set; } public List> Actions { get; set; } IEvent IEventData.Event => Event; public IEnumerable ReferencedVariables => Triggers.SelectMany(x => x.ReferencedVariables) .Concat(Actions.SelectMany(x => x.ReferencedVariables)) .Distinct(); public EventData(T eventData) { Event = eventData; Triggers = new List>(); Actions = new List>(); } public Notification GenerateNotification(TDataModel model) => Event.GenerateNotification(model); } public static class EventUtils { #region Serialisation private class WritablePropertiesOnlyResolver : DefaultContractResolver { protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { IList props = base.CreateProperties(type, memberSerialization); return props.Where(p => p.Writable).ToList(); } } private static JsonSerializerSettings SerializationSettings() { var settings = Serialization.CreateSerializerSettings(); settings.TypeNameHandling = TypeNameHandling.Auto; settings.ContractResolver = new WritablePropertiesOnlyResolver(); return settings; } public static string Serialize(IEventData data) { return JsonConvert.SerializeObject(data, typeof(IEventData), SerializationSettings()); } public static IEventData Deserialize(string json) { return JsonConvert.DeserializeObject(json, SerializationSettings())!; } #endregion #region Event Types private static Dictionary? _eventTypes; private static Dictionary? _triggerTypes; private static Dictionary? _actionTypes; private static Dictionary>? _eventTriggerTypes; private static Dictionary>? _eventActionTypes; [MemberNotNullWhen(true, nameof(_eventTypes), nameof(_triggerTypes), nameof(_actionTypes), nameof(_eventTriggerTypes), nameof(_eventActionTypes))] private static bool _loadedTypes { get; set; } [MemberNotNull(nameof(_eventTypes), nameof(_triggerTypes), nameof(_actionTypes), nameof(_eventTriggerTypes), nameof(_eventActionTypes))] private static void LoadTypes() { if (_loadedTypes) return; _loadedTypes = true; _eventTypes = new(); _triggerTypes = new(); _actionTypes = new(); _eventTriggerTypes = new(); _eventActionTypes = new(); foreach(var type in CoreUtils.TypeList(x => true)) { if (type.HasInterface(typeof(IEvent<>))) { if (type.GetConstructor([]) is not null) { _eventTypes[type.Name] = type; } } else if (type.GetInterfaceDefinition(typeof(IEventTrigger<,>)) is Type eventTriggerInterface) { if (type.GetConstructor([]) is not null) { _triggerTypes[type.Name] = type; var eventType = eventTriggerInterface.GenericTypeArguments[0]; eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType; _eventTriggerTypes.GetValueOrAdd(eventType).Add(type); } } else if (type.GetInterfaceDefinition(typeof(IEventAction<>)) is Type eventActionInterface) { if (type.GetConstructor([]) is not null) { _actionTypes[type.Name] = type; var eventType = eventActionInterface.GenericTypeArguments[0]; eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType; _eventActionTypes.GetValueOrAdd(eventType).Add(type); } } } } public static IEnumerable GetEventTriggerTypes(Type eventType) { LoadTypes(); eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType; return _eventTriggerTypes.GetValueOrDefault(eventType) ?? Enumerable.Empty(); } public static IEnumerable GetEventActionTypes(Type eventType) { LoadTypes(); eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType; return _eventActionTypes.GetValueOrDefault(eventType) ?? Enumerable.Empty(); } public static Type GetEventType(string type) { LoadTypes(); return _eventTypes[type]; // Force exception if not a valid type name. } public static Type GetTriggerType(string type) { LoadTypes(); return _triggerTypes[type]; // Force exception if not a valid type name. } public static Type GetActionType(string type) { LoadTypes(); return _actionTypes[type]; // Force exception if not a valid type name. } #endregion #region Execution private static bool Check(EventData ev, TDataModel dataModel) where T : IEvent where TDataModel : IEventDataModel { return ev.Triggers.Any(x => x.Check(dataModel)); } public static void Run(IStore store, Event ev, EventData evData, TDataModel dataModel) where T : IEvent where TDataModel : IEventDataModel { if (!Check(evData, dataModel)) return; foreach(var action in evData.Actions) { IEvent.RunAction(evData.Event, dataModel, action); } NotifySubscribers(store, ev, evData, dataModel); } private static void NotifySubscribers(IStore store, Event ev, EventData evData, TDataModel dataModel) where T : IEvent where TDataModel : IEventDataModel { string? description; if (ev.NotificationExpression.IsNullOrWhiteSpace()) { description = null; } else { var descriptionExpr = new CoreExpression(ev.NotificationExpression, typeof(string)); if(!descriptionExpr.TryEvaluate(dataModel).Get(out description, out var error)) { CoreUtils.LogException(store.UserID, error, extra: "Error notifying subscribers", store.Logger); return; } } var subscribers = store.Provider.Query( new Filter(x => x.Event.ID).IsEqualTo(ev.ID), Columns.None().Add(x => x.Employee.ID)) .ToArray(); var notifications = new List(); foreach(var subscriber in subscribers) { var notification = evData.GenerateNotification(dataModel); notification.Employee.CopyFrom(subscriber.Employee); if(description is not null) { notification.Description = description; } notifications.Add(notification); } store.Provider.Save(notifications); } #endregion #region Event Cache private static bool _loadedCache = false; private static Dictionary>> _entityEvents = new(); private static Dictionary _entityEventMap = new(); public static void ReloadCache(IProvider provider) { _loadedCache = true; _entityEvents.Clear(); _entityEventMap.Clear(); var events = provider.Query( new Filter(x => x.Enabled).IsEqualTo(true), Columns.None().Add(x => x.ID).Add(x => x.Code).Add(x => x.EventType).Add(x => x.Data)) .ToObjects(); foreach(var ev in events) { AddEvent(provider, ev); } } private static void AddEntityEvent(Event ev) { if(ev.Data is not null && ev.Data.Length != 0) { var data = Deserialize(ev.Data); var entityType = data.Event.GetType().GenericTypeArguments[0]; var list = _entityEvents.GetValueOrAdd(ev.EventType).GetValueOrAdd(entityType); list.RemoveAll(x => x.Event.ID == ev.ID); list.Add((ev, data)); _entityEventMap[ev.ID] = (ev.EventType, entityType); } } private static void RemoveEntityEvent(Event ev) { if(_entityEventMap.TryGetValue(ev.ID, out var item)) { if(_entityEvents.TryGetValue(item.Item1, out var eventTypeEvents)) { if(eventTypeEvents.TryGetValue(item.Item2, out var entityEvents)) { entityEvents.RemoveAll(x => x.Event.ID == ev.ID); if(entityEvents.Count == 0) { eventTypeEvents.Remove(item.Item2); } } if(eventTypeEvents.Count == 0) { _entityEvents.Remove(item.Item1); } } _entityEventMap.Remove(ev.ID); } } public static void RemoveEvent(IProvider provider, Event ev) { if (!_loadedCache) ReloadCache(provider); switch (ev.EventType) { case EventType.AfterSave: RemoveEntityEvent(ev); break; } } public static void AddEvent(IProvider provider, Event ev) { if (!_loadedCache) ReloadCache(provider); switch (ev.EventType) { case EventType.AfterSave: AddEntityEvent(ev); break; } } #endregion #region Public Event Running Interface public static void AfterSave(IStore store, T entity) where T : Entity, new() { if (!_loadedCache) ReloadCache(store.Provider); var events = _entityEvents.GetValueOrDefault(EventType.AfterSave) ?.GetValueOrDefault(typeof(T)); if (events is null) return; foreach(var ev in events) { var eventData = (ev.EventData as EventData, SaveEventDataModel>)!; var model = new SaveEventDataModel(entity, store); eventData.Event.Init(store, eventData, model); Run(store, ev.Event, eventData, model); } } #endregion } #region DataModel Definition public interface IEventVariable { string Name { get; set; } Type Type { get; set; } } public class StandardEventVariable : IEventVariable { public string Name { get; set; } public Type Type { get; set; } public StandardEventVariable(string name, Type type) { Name = name; Type = type; } } public class ListEventVariable : IEventVariable { public string Name { get; set; } public Type Type { get; set; } public ListEventVariable(string name, Type type) { Name = name; Type = type; } } public interface IEventDataModelDefinition { IEnumerable GetVariables(); IEventVariable? GetVariable(string name); } #endregion #region DataModel public interface IEventDataModel : IVariableProvider { bool TryGetVariable(string name, out object? value); bool IVariableProvider.TryGetValue(string variableName, out object? value) { return TryGetVariable(variableName, out value); } T RootModel() where T : IEventDataModel { if(this is IChildEventDataModel child) { return child.Parent.RootModel(); } else if(this is T root) { return root; } else { throw new Exception($"Root model of wrong type; expected {typeof(T).FullName}; got {GetType().FullName}"); } } } public interface ITypedEventDataModel : IEventDataModel { public Type EntityType { get; } } public interface IChildEventDataModel : IEventDataModel { IEventDataModel Parent { get; } } public class ChildEventDataModel : IChildEventDataModel { public IEventDataModel Parent { get; set; } public Dictionary Values { get; set; } = new Dictionary(); public ChildEventDataModel(IEventDataModel parent) { Parent = parent; } public bool TryGetVariable(string name, out object? value) { return Values.TryGetValue(name, out value) || Parent.TryGetVariable(name, out value); } public void Init(IEventData data) { } } #endregion public interface IEvent { IEventDataModelDefinition DataModelDefinition(); public static void RunAction(IEvent ev, IEventDataModel model, IEventAction action) { var dataModelDef = ev.DataModelDefinition(); var values = new List<(string name, object? value)[]>() { Array.Empty<(string, object?)>() }; foreach(var variable in action.ReferencedVariables) { var varDef = dataModelDef.GetVariable(variable); if(varDef is ListEventVariable) { if (model.TryGetVariable(varDef.Name, out var list) && list is IEnumerable enumerable) { var oldValues = values; values = new List<(string name, object? value)[]>(); foreach(var item in enumerable) { foreach(var valueList in oldValues) { values.Add(valueList.Concatenate(new (string name, object? value)[] { (varDef.Name, item) })); } } } else { values.Clear(); break; } } } if(values.Count > 0 && (values.Count > 1 || values[0].Length > 0)) { var subModel = new ChildEventDataModel(model); foreach(var valueSet in values) { subModel.Values.Clear(); foreach(var (name, value) in valueSet) { subModel.Values[name] = value; } action.Execute(subModel); } } else { action.Execute(model); } } } /// /// Indicates that this event type has properties that can be edited; is used to edit these properties /// with a dynamic editor. /// public interface IPropertiesEvent where TProperties : BaseObject, new() { /// /// Create the properties object for editing. /// TProperties GetProperties(); /// /// Given an edited properties object, save them again. /// void SetProperties(TProperties properties); } public interface IEvent : IEvent { void Init(IStore store, IEventData data, TDataModel model); Notification GenerateNotification(TDataModel model); } public interface IEventTrigger { IEnumerable ReferencedVariables { get; } string Description { get; } } public interface IEventTrigger : IEventTrigger where TEvent : IEvent where TDataModel : IEventDataModel { bool Check(TDataModel dataModel); } public interface IEventTriggerParent : IEventTrigger where TEvent : IEvent where TDataModel : IEventDataModel { public List> ChildTriggers { get; } } public class AndTrigger : IEventTrigger, IEventTriggerParent where TEvent : IEvent where TDataModel : IEventDataModel { public List> ChildTriggers { get; set; } = new(); public IEnumerable ReferencedVariables => ChildTriggers.SelectMany(x => x.ReferencedVariables); public string Description => "And"; public bool Check(TDataModel dataModel) { return ChildTriggers.All(x => x.Check(dataModel)); } } public class OrTrigger : IEventTrigger, IEventTriggerParent where TEvent : IEvent where TDataModel : IEventDataModel { public List> ChildTriggers { get; set; } = new(); public IEnumerable ReferencedVariables => ChildTriggers.SelectMany(x => x.ReferencedVariables); public string Description => "Or"; public bool Check(TDataModel dataModel) { return ChildTriggers.Any(x => x.Check(dataModel)); } } public interface IEventAction { IEnumerable ReferencedVariables { get; } object? Execute(IEventDataModel dataModel); string Description { get; } } public interface IEventAction : IEventAction where TEvent : IEvent { }