using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp.Net;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
namespace PepperDash.Essentials.AppServer.Messengers
{
///
/// Provides a messaging bridge
///
public abstract class MessengerBase : EssentialsDevice, IMobileControlMessengerWithSubscriptions
{
///
/// The device this messenger is associated with
///
protected IKeyName _device;
///
/// Enable subscriptions
///
protected bool enableMessengerSubscriptions;
///
/// List of clients subscribed to this messenger
///
///
/// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection.
///
private readonly HashSet subscriberIds = new HashSet();
///
/// Lock object for thread-safe access to SubscriberIds
///
private readonly object _subscriberLock = new object();
private readonly List _deviceInterfaces;
private readonly Dictionary> _actions = new Dictionary>();
///
/// Gets the DeviceKey
///
public string DeviceKey => _device?.Key ?? "";
///
/// Gets or sets the AppServerController
///
public IMobileControl AppServerController { get; private set; }
///
/// Gets or sets the MessagePath
///
public string MessagePath { get; private set; }
///
///
///
///
///
protected MessengerBase(string key, string messagePath)
: base(key)
{
Key = key;
if (string.IsNullOrEmpty(messagePath))
throw new ArgumentException("messagePath must not be empty or null");
MessagePath = messagePath;
}
///
/// Constructor for a messenger associated with a device
///
///
///
///
protected MessengerBase(string key, string messagePath, IKeyName device)
: this(key, messagePath)
{
_device = device;
_deviceInterfaces = GetInterfaces(_device as Device);
}
///
/// Gets the interfaces implmented on the device
///
///
///
private List GetInterfaces(Device device)
{
return device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List();
}
///
/// Registers this messenger with appserver controller
///
///
public void RegisterWithAppServer(IMobileControl appServerController)
{
AppServerController = appServerController ?? throw new ArgumentNullException("appServerController");
AppServerController.AddAction(this, HandleMessage);
RegisterActions();
}
///
/// Register this messenger with appserver controller
///
/// Parent controller for this messenger
/// Enable subscriptions
public void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions)
{
this.enableMessengerSubscriptions = enableMessengerSubscriptions;
AppServerController = appServerController ?? throw new ArgumentNullException("appServerController");
AppServerController.AddAction(this, HandleMessage);
RegisterActions();
}
private void HandleMessage(string path, string id, JToken content)
{
// replace base path with empty string. Should leave something like /fullStatus
var route = path.Replace(MessagePath, string.Empty);
if (!_actions.TryGetValue(route, out var action))
{
return;
}
this.LogDebug("Executing action for path {path}", path);
action(id, content);
}
///
/// Adds an action for a given path
///
///
///
protected void AddAction(string path, Action action)
{
if (_actions.ContainsKey(path))
{
return;
}
_actions.Add(path, action);
}
///
/// GetActionPaths method
///
public List GetActionPaths()
{
return _actions.Keys.ToList();
}
///
/// Removes an action for a given path
///
///
protected void RemoveAction(string path)
{
if (!_actions.ContainsKey(path))
{
return;
}
_actions.Remove(path);
}
///
/// Implemented in extending classes. Wire up API calls and feedback here
///
protected virtual void RegisterActions()
{
}
///
/// Add client to the susbscription list for unsolicited feedback
///
/// Client ID to add
protected void SubscribeClient(string clientId)
{
if (!enableMessengerSubscriptions)
{
return;
}
lock (_subscriberLock)
{
if (!subscriberIds.Add(clientId))
{
this.LogVerbose("Client {clientId} already subscribed", clientId);
return;
}
}
this.LogDebug("Client {clientId} subscribed", clientId);
}
///
/// Remove a client from the subscription list
///
/// Client ID to remove
public void UnsubscribeClient(string clientId)
{
if (!enableMessengerSubscriptions)
{
return;
}
bool wasSubscribed;
lock (_subscriberLock)
{
wasSubscribed = subscriberIds.Contains(clientId);
if (wasSubscribed)
{
subscriberIds.Remove(clientId);
}
}
if (!wasSubscribed)
{
this.LogVerbose("Client with ID {clientId} is not subscribed", clientId);
return;
}
this.LogDebug("Client with ID {clientId} unsubscribed", clientId);
}
///
/// Helper for posting status message
///
///
/// Optional client id that will direct the message back to only that client
protected void PostStatusMessage(DeviceStateMessageBase message, string clientId = null)
{
try
{
if (message == null)
{
throw new ArgumentNullException("message");
}
if (_device == null)
{
throw new ArgumentNullException("device");
}
message.SetInterfaces(_deviceInterfaces);
message.Key = _device.Key;
message.Name = _device.Name;
var token = JToken.FromObject(message);
PostStatusMessage(token, MessagePath, clientId);
}
catch (Exception ex)
{
this.LogError("Exception posting status message for {messagePath} to {clientId}: {message}", MessagePath, clientId ?? "all clients", ex.Message);
this.LogDebug(ex, "Stack trace: ");
}
}
///
/// Helper for posting status message
///
///
///
/// Optional client id that will direct the message back to only that client
protected void PostStatusMessage(string type, DeviceStateMessageBase deviceState, string clientId = null)
{
try
{
//Debug.Console(2, this, "*********************Setting DeviceStateMessageProperties on MobileControlResponseMessage");
deviceState.SetInterfaces(_deviceInterfaces);
deviceState.Key = _device.Key;
deviceState.Name = _device.Name;
deviceState.MessageBasePath = MessagePath;
var token = JToken.FromObject(deviceState);
PostStatusMessage(token, type, clientId);
}
catch (Exception ex)
{
this.LogError("Exception posting status message for {type} to {clientId}: {message}", type, clientId ?? "all clients", ex.Message);
this.LogDebug(ex, "Stack trace: ");
}
}
///
/// Helper for posting status message
///
///
///
/// Optional client id that will direct the message back to only that client
protected void PostStatusMessage(JToken content, string type = "", string clientId = null)
{
try
{
// Allow for legacy method to continue without subscriptions
if (!enableMessengerSubscriptions)
{
AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content });
return;
}
// handle subscription feedback
// If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties
if (string.IsNullOrEmpty(clientId))
{
// Create a snapshot of subscribers to avoid collection modification during iteration
List subscriberSnapshot;
lock (_subscriberLock)
{
subscriberSnapshot = new List(subscriberIds);
}
foreach (var client in subscriberSnapshot)
{
AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content });
}
return;
}
SubscribeClient(clientId);
AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content });
}
catch (Exception ex)
{
this.LogError("Exception posting status message: {message}", ex.Message);
this.LogDebug(ex, "Stack Trace: ");
}
}
///
/// Helper for posting event message
///
///
protected void PostEventMessage(DeviceEventMessageBase message)
{
message.Key = _device.Key;
message.Name = _device.Name;
AppServerController?.SendMessageObject(new MobileControlMessage
{
Type = $"/event{MessagePath}/{message.EventType}",
Content = JToken.FromObject(message),
});
}
///
/// Helper for posting event message
///
///
///
protected void PostEventMessage(DeviceEventMessageBase message, string eventType)
{
message.Key = _device.Key;
message.Name = _device.Name;
message.EventType = eventType;
AppServerController?.SendMessageObject(new MobileControlMessage
{
Type = $"/event{MessagePath}/{eventType}",
Content = JToken.FromObject(message),
});
}
///
/// Helper for posting event message with no content
///
///
protected void PostEventMessage(string eventType)
{
AppServerController?.SendMessageObject(new MobileControlMessage
{
Type = $"/event{MessagePath}/{eventType}",
Content = JToken.FromObject(new { }),
});
}
}
}