using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Reflection;
using Crestron.SimplSharp;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Essentials.Core.Config;
using Serilog.Events;
namespace PepperDash.Essentials.Core;
///
/// Represents a wrapper for a device factory, encapsulating the type of device, a description, and a factory method
/// for creating device instances.
///
/// This class is designed to provide a convenient way to store and manage metadata and factory methods
/// for creating devices based on a given configuration.
public class DeviceFactoryWrapper
{
///
/// Gets or sets the type associated with the current instance.
///
public Type Type { get; set; }
///
/// Gets or sets the description associated with the object.
///
public string Description { get; set; }
///
/// Gets or sets the factory method used to create an instance based on the provided .
///
/// The factory method allows customization of how instances are created for
/// specific inputs. Ensure the delegate is not null before invoking it.
public Func FactoryMethod { get; set; }
///
/// Initializes a new instance of the class with default values.
///
/// The property is initialized to , and the property is set to "Not Available".
public DeviceFactoryWrapper()
{
Type = null;
Description = "Not Available";
}
}
///
/// Provides functionality for managing and registering device factories, including loading plugin-based factories and
/// retrieving devices based on their configuration.
///
/// The class is responsible for discovering and registering device factories
/// from plugins, as well as providing methods to retrieve devices based on their configuration. It maintains a
/// collection of factory methods that are keyed by device type names, allowing for extensibility through plugins. This
/// class also handles metadata retrieval and secret management for device configurations.
public class DeviceFactory
{
///
/// Initializes a new instance of the class and loads all available device factories
/// from the current assembly.
///
/// This constructor scans the executing assembly for types that implement the interface and are not abstract or interfaces. For each valid type, an instance is
/// created and passed to the LoadDeviceFactories method for further processing. If a type cannot be
/// instantiated, an informational log message is generated, and the process continues with the remaining
/// types.
public DeviceFactory()
{
var programAssemblies = Directory.GetFiles(InitialParametersClass.ProgramDirectory.ToString(), "*.dll");
foreach (var assembly in programAssemblies)
{
try
{
Assembly.LoadFrom(assembly);
}
catch (Exception e)
{
Debug.LogError("Unable to load assembly: {assemblyName} - {message}", assembly, e.Message);
}
}
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
// Loop through all loaded assemblies that contain at least 1 type that implements IDeviceFactory
foreach (var assembly in loadedAssemblies)
{
Debug.LogDebug("loaded assembly: {assemblyName}", assembly.GetName().Name);
PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly);
var types = assembly.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract);
if (types == null || !types.Any())
{
Debug.LogDebug("No DeviceFactory types found in assembly: {assemblyName}", assembly.GetName().Name);
continue;
}
foreach (var type in types)
{
try
{
var factory = (IDeviceFactory)Activator.CreateInstance(type);
LoadDeviceFactories(factory);
}
catch (Exception e)
{
Debug.LogError("Unable to load type: '{message}' DeviceFactory: {type}", e.Message, type.Name);
}
}
}
}
///
/// Loads device factories from the specified plugin device factory and registers them for use.
///
/// This method retrieves metadata from the provided , including
/// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type
/// names are converted to lowercase for registration.
/// The plugin device factory that provides the device types, descriptions, and factory methods to be registered.
private static void LoadDeviceFactories(IDeviceFactory deviceFactory)
{
foreach (var typeName in deviceFactory.TypeNames)
{
//Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName);
var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[];
string description = descriptionAttribute[0].Description;
var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[];
AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice);
}
}
///
/// A dictionary of factory methods, keyed by config types, added by plugins.
/// These methods are looked up and called by GetDevice in this class.
///
private static readonly Dictionary FactoryMethods =
new(StringComparer.OrdinalIgnoreCase);
///
/// Registers a factory method for creating instances of a specific type.
///
/// This method associates a type name with a factory method, allowing instances of the type to
/// be created dynamically. The factory method is stored internally and can be retrieved or invoked as
/// needed.
/// The name of the type for which the factory method is being registered. This value cannot be null or empty.
/// A delegate that defines the factory method. The delegate takes a parameter and
/// returns an instance of .
public static void AddFactoryForType(string typeName, Func method)
{
FactoryMethods.Add(typeName, new DeviceFactoryWrapper() { FactoryMethod = method });
}
///
/// Registers a factory method for creating instances of a specific device type.
///
/// If a factory method for the specified already exists, the method
/// will not overwrite it and will log an informational message instead.
/// The unique name of the device type. This serves as the key for identifying the factory method.
/// A brief description of the device type. This is used for informational purposes.
/// The of the device being registered. This represents the runtime type of the device.
/// A factory method that takes a as input and returns an instance of .
public static void AddFactoryForType(string typeName, string description, Type Type, Func method)
{
if (FactoryMethods.ContainsKey(typeName))
{
Debug.LogInformation("Unable to add type: '{typeName}'. Already exists in DeviceFactory", typeName);
return;
}
var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method };
FactoryMethods.Add(typeName, wrapper);
}
private static void CheckForSecrets(IEnumerable obj)
{
foreach (var prop in obj.Where(prop => prop.Value as JObject != null))
{
if (prop.Name.Equals("secret", StringComparison.CurrentCultureIgnoreCase))
{
var secret = GetSecret(prop.Children().First().ToObject());
prop.Parent.Replace(secret);
}
if (prop.Value is not JObject recurseProp) return;
CheckForSecrets(recurseProp.Properties());
}
}
private static string GetSecret(SecretsPropertiesConfig data)
{
var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider);
if (secretProvider == null) return null;
var secret = secretProvider.GetSecret(data.Key);
if (secret != null) return (string)secret.Value;
Debug.LogMessage(LogEventLevel.Debug,
"Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider",
data.Provider, data.Key);
return string.Empty;
}
///
/// Creates and returns a device instance based on the provided .
///
/// This method attempts to create a device using the type specified in the
/// parameter. If the type corresponds to a registered factory method, the device is created and returned. If the
/// type is unrecognized or an exception occurs, the method logs the error and returns .
/// The configuration object containing the key, name, type, and properties required to create the device.
/// An instance of a device that implements , or if the device type is
/// not recognized or an error occurs during creation.
public static IKeyed GetDevice(DeviceConfig dc)
{
try
{
var localDc = new DeviceConfig(dc);
var key = localDc.Key;
var name = localDc.Name;
var type = localDc.Type;
var properties = localDc.Properties;
var typeName = localDc.Type.ToLower();
if (properties is JObject jObject)
{
var jProp = jObject.Properties();
CheckForSecrets(jProp);
}
if (!FactoryMethods.TryGetValue(typeName, out var wrapper))
{
Debug.LogWarning("Device type '{typeName}' not found in DeviceFactory", typeName);
return null;
}
Debug.LogInformation("Loading '{type}' from {assemblyName}", typeName, wrapper.Type.Assembly.FullName);
// Check for types that have been added by plugin dlls.
return wrapper.FactoryMethod(localDc);
}
catch (Exception ex)
{
Debug.LogError(ex, "Exception occurred while creating device {0}: {1}", null, dc.Key, ex.Message);
return null;
}
}
///
/// Displays a list of device factory types that match the specified filter.
///
/// The method outputs the filtered list of device factory types to the console, including their
/// key, type, and description. If a type is not specified by the plugin, it will be displayed as "Not Specified by
/// Plugin."
/// A string used to filter the device factory types by their keys. If the filter is null or empty, all device
/// factory types are displayed.
public static void GetDeviceFactoryTypes(string filter)
{
var types = !string.IsNullOrEmpty(filter)
? FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value)
: FactoryMethods;
CrestronConsole.ConsoleCommandResponse("Device Types:");
foreach (var type in types.OrderBy(t => t.Key))
{
var description = type.Value.Description;
var Type = "Not Specified by Plugin";
if (type.Value.Type != null)
{
Type = type.Value.Type.FullName;
}
CrestronConsole.ConsoleCommandResponse(
"Type: '{0}'\r\n" +
" Type: '{1}'\r\n" +
" Description: {2}{3}", type.Key, Type, description, CrestronEnvironment.NewLine);
}
}
///
/// Retrieves a dictionary of device factory wrappers, optionally filtered by a specified string.
///
/// A string used to filter the dictionary keys. Only entries with keys containing the specified filter will be
/// included. If or empty, all entries are returned.
/// A dictionary where the keys are strings representing device identifiers and the values are instances. The dictionary may be empty if no entries match the filter.
public static Dictionary GetDeviceFactoryDictionary(string filter)
{
return string.IsNullOrEmpty(filter)
? FactoryMethods
: FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value);
}
}