/*---------------------------------------------------------*\
| PluginManager.cpp                                         |
|                                                           |
|   OpenRGB plugin manager                                  |
|                                                           |
|   This file is part of the OpenRGB project                |
|   SPDX-License-Identifier: GPL-2.0-or-later               |
\*---------------------------------------------------------*/

#include "LogManager.h"
#include "filesystem.h"
#include "PluginManager.h"
#include "OpenRGBPluginAPI.h"
#include "SettingsManager.h"
#include "ResourceManager.h"

#ifdef _WIN32
#include <Windows.h>
#endif

/*---------------------------------------------------------*\
| PluginManager name for log entries                        |
\*---------------------------------------------------------*/
const char* PLUGINMANAGER = "PluginManager";

void PluginManagerProfileManagerCallback(void * this_ptr, unsigned int update_reason)
{
    PluginManager * this_obj = (PluginManager *)this_ptr;

    this_obj->ProfileManagerCallback(update_reason);
}

void PluginManagerResourceManagerCallback(void * this_ptr, unsigned int update_reason)
{
    PluginManager * this_obj = (PluginManager *)this_ptr;

    this_obj->ResourceManagerCallback(update_reason);
}

void PluginManagerSettingsManagerCallback(void * this_ptr, unsigned int update_reason)
{
    PluginManager * this_obj = (PluginManager *)this_ptr;

    this_obj->SettingsManagerCallback(update_reason);
}

PluginManager::PluginManager()
{
    /*-----------------------------------------------------*\
    | Initialize plugin manager class variables             |
    \*-----------------------------------------------------*/
    AddPluginCallbackVal    = nullptr;
    AddPluginCallbackArg    = nullptr;
    RemovePluginCallbackVal = nullptr;
    RemovePluginCallbackArg = nullptr;

    /*-----------------------------------------------------*\
    | Create OpenRGB plugins directory                      |
    \*-----------------------------------------------------*/
    filesystem::path plugins_dir = ResourceManager::get()->GetConfigurationDirectory() / plugins_path;

    filesystem::create_directories(plugins_dir);

    /*-----------------------------------------------------*\
    | Register callbacks                                    |
    \*-----------------------------------------------------*/
    ResourceManager::get()->GetProfileManager()->RegisterProfileManagerCallback(PluginManagerProfileManagerCallback, this);
    ResourceManager::get()->RegisterResourceManagerCallback(PluginManagerResourceManagerCallback, this);
    ResourceManager::get()->GetSettingsManager()->RegisterSettingsManagerCallback(PluginManagerSettingsManagerCallback, this);
}

PluginManager::~PluginManager()
{
    /*-----------------------------------------------------*\
    | Unegister callbacks                                   |
    \*-----------------------------------------------------*/
    ResourceManager::get()->GetProfileManager()->UnregisterProfileManagerCallback(PluginManagerProfileManagerCallback, this);
    ResourceManager::get()->UnregisterResourceManagerCallback(PluginManagerResourceManagerCallback, this);
    ResourceManager::get()->GetSettingsManager()->UnregisterSettingsManagerCallback(PluginManagerSettingsManagerCallback, this);
}

void PluginManager::RegisterAddPluginCallback(AddPluginCallback new_callback, void * new_callback_arg)
{
    AddPluginCallbackVal    = new_callback;
    AddPluginCallbackArg    = new_callback_arg;
}

void PluginManager::RegisterRemovePluginCallback(RemovePluginCallback new_callback, void * new_callback_arg)
{
    RemovePluginCallbackVal = new_callback;
    RemovePluginCallbackArg = new_callback_arg;
}

void PluginManager::ScanAndLoadPlugins()
{
    /*-----------------------------------------------------*\
    | Get the user plugins directory                        |
    |                                                       |
    | The user plugins directory is a directory named       |
    | "plugins" in the configuration directory              |
    \*-----------------------------------------------------*/
    filesystem::path plugins_dir = ResourceManager::get()->GetConfigurationDirectory() / plugins_path;
    ScanAndLoadPluginsFrom(plugins_dir, false);

#ifdef OPENRGB_SYSTEM_PLUGIN_DIRECTORY
    /*-----------------------------------------------------*\
    | Get the system plugins directory                      |
    |                                                       |
    | The system plugin directory can be set during build   |
    | time, e.g. by the package maintainer to load plugins  |
    | installed via package manager                         |
    \*-----------------------------------------------------*/
    ScanAndLoadPluginsFrom(OPENRGB_SYSTEM_PLUGIN_DIRECTORY, true);
#endif

#ifdef _WIN32
    /*-----------------------------------------------------*\
    | Get the exe folder plugins directory (Windows)        |
    |                                                       |
    | On Windows, system plugins are located in a folder    |
    | called "plugins" inside the folder where the          |
    | OpenRGB.exe file is installed.  Typically,            |
    | C:\Program Files\OpenRGB but other install paths are  |
    | allowed.                                              |
    \*-----------------------------------------------------*/
    char path[MAX_PATH];
    GetModuleFileName(NULL, path, MAX_PATH);

    filesystem::path exe_dir(path);
    exe_dir = exe_dir.remove_filename() / plugins_path;

    ScanAndLoadPluginsFrom(exe_dir, true);
#endif
}

void PluginManager::ScanAndLoadPluginsFrom(const filesystem::path & plugins_dir, bool is_system)
{
    if(is_system)
    {
        LOG_TRACE("[%s] Scanning system plugin directory: %s", PLUGINMANAGER, plugins_dir.generic_u8string().c_str());
    }
    else
    {
        LOG_TRACE("[%s] Scanning user plugin directory: %s", PLUGINMANAGER, plugins_dir.generic_u8string().c_str());
    }

    if(!filesystem::is_directory(plugins_dir))
    {
        return;
    }

    /*-----------------------------------------------------*\
    | Get a list of all files in the plugins directory      |
    \*-----------------------------------------------------*/
    for(const filesystem::directory_entry& entry: filesystem::directory_iterator(plugins_dir))
    {
        if(filesystem::is_directory(entry.path()))
        {
            continue;
        }

        filesystem::path plugin_path = entry.path();
        LOG_TRACE("[%s] Found plugin file %s", PLUGINMANAGER, plugin_path.filename().generic_u8string().c_str());
        AddPlugin(plugin_path, is_system);
    }
}

void PluginManager::AddPlugin(const filesystem::path& path, bool is_system)
{
    /*-----------------------------------------------------*\
    | Prepare variables and open plugin settings            |
    \*-----------------------------------------------------*/
    std::string             path_string     = path.generic_u8string();
    OpenRGBPluginInterface* plugin          = nullptr;
    unsigned int            plugin_idx;
    json                    plugin_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("Plugins");

    /*-----------------------------------------------------*\
    | Remove this plugin if it is on the remove list        |
    \*-----------------------------------------------------*/
    if(plugin_settings.contains("plugins_remove"))
    {
        for(std::size_t plugin_remove_idx = 0; plugin_remove_idx < plugin_settings["plugins_remove"].size(); plugin_remove_idx++)
        {
            LOG_WARNING("[%s] Checking remove %d, %s", PLUGINMANAGER, plugin_remove_idx, to_string(plugin_settings["plugins_remove"][plugin_remove_idx]).c_str());

            if(plugin_settings["plugins_remove"][plugin_remove_idx] == path_string)
            {
                /*-----------------------------------------*\
                | Delete the plugin file                    |
                \*-----------------------------------------*/
                filesystem::remove(path);
            }

            /*---------------------------------------------*\
            | Erase the plugin from the remove list         |
            \*---------------------------------------------*/
            plugin_settings["plugins_remove"].erase(plugin_remove_idx);

            ResourceManager::get()->GetSettingsManager()->SetSettings("Plugins", plugin_settings);
            ResourceManager::get()->GetSettingsManager()->SaveSettings();
        }
    }

    /*-----------------------------------------------------*\
    | Search active plugins to see if this path already     |
    | exists                                                |
    \*-----------------------------------------------------*/
    for(plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(path == ActivePlugins[plugin_idx].path)
        {
            LOG_WARNING("[%s] Plugin file %s already in list, skipping.", PLUGINMANAGER, path_string.c_str());
            return;
        }
    }

    /*-----------------------------------------------------*\
    | Create a QPluginLoader and extract the metadata       |
    \*-----------------------------------------------------*/
    QPluginLoader*      loader              = new QPluginLoader(QString::fromStdString(path_string));
    QJsonObject         metadata            = loader->metaData();
    unsigned int        plugin_api_version  = 0;
    QString             plugin_commit;
    QString             plugin_id           = "incompatible";
    QString             plugin_name         = "Incompatible Plugin";
    QString             plugin_url;
    QString             plugin_version;

    if(metadata.contains("MetaData"))
    {
        metadata                            = metadata.value("MetaData").toObject();
    }
    else
    {
        LOG_WARNING("[%s] Plugin %s does not have a MetaData field, skipping.", PLUGINMANAGER, path_string.c_str());
        return;
    }

    if(metadata.contains("OpenRGBPluginAPIVersion"))
    {
        plugin_api_version                  = metadata.value("OpenRGBPluginAPIVersion").toInt();
    }
    else
    {
        LOG_WARNING("[%s] Plugin %s does not have an OpenRGBPluginAPIVersion field, skipping.", PLUGINMANAGER, path_string.c_str());
        return;
    }

    if(metadata.contains("Id"))
    {
        plugin_id                           = metadata.value("Id").toString();
    }
    else
    {
        LOG_WARNING("[%s] Plugin %s does not have an Id field, skipping.", PLUGINMANAGER, path_string.c_str());
        return;
    }

    if(metadata.contains("Commit"))
    {
        plugin_commit                       = metadata.value("Commit").toString();
    }

    if(metadata.contains("Name"))
    {
        plugin_name                         = metadata.value("Name").toString();
    }

    if(metadata.contains("Url"))
    {
        plugin_url                          = metadata.value("Url").toString();
    }

    if(metadata.contains("VersionStr"))
    {
        plugin_version                      = metadata.value("VersionStr").toString();
    }

    /*-----------------------------------------------------*\
    | Create a new plugin entry to add to the list          |
    \*-----------------------------------------------------*/
    OpenRGBPluginEntry  entry;

    entry.api                               = new OpenRGBPluginAPI();
    entry.api_version                       = plugin_api_version;
    entry.enabled                           = false;
    entry.info.Commit                       = plugin_commit.toStdString();
    entry.info.Description                  = "This plugin is not compatible with this version of OpenRGB.";
    entry.info.Name                         = plugin_name.toStdString();
    entry.info.URL                          = plugin_url.toStdString();
    entry.info.Version                      = plugin_version.toStdString();
    entry.id                                = plugin_id;
    entry.incompatible                      = true;
    entry.is_system                         = is_system;
    entry.loader                            = loader;
    entry.path                              = path_string;
    entry.plugin                            = plugin;
    entry.widget                            = nullptr;

    /*-----------------------------------------------------*\
    | If metadata indicates a mismatching API version,      |
    | skip loading                                          |
    \*-----------------------------------------------------*/
    if(plugin_api_version != OPENRGB_PLUGIN_API_VERSION)
    {
        LOG_WARNING("[%s] Plugin %s has an incompatible API version", PLUGINMANAGER, path_string.c_str());
        goto add_plugin_entry;
    }

    /*-----------------------------------------------------*\
    | Check to see if this plugin's ID already exists       |
    \*-----------------------------------------------------*/
    for(plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(plugin_id == ActivePlugins[plugin_idx].id && !ActivePlugins[plugin_idx].incompatible)
        {
            entry.info.Description          = "This plugin has a duplicate ID as an already loaded plugin and cannot be loaded.";
            LOG_WARNING("[%s] Plugin ID %s already in list.", PLUGINMANAGER, plugin_id.toStdString().c_str());
            goto add_plugin_entry;
        }
    }

    {
        QObject*        instance            = loader->instance();

        if(!loader->isLoaded())
        {
            LOG_WARNING("[%s] Plugin %s cannot be loaded: %s", PLUGINMANAGER, path_string.c_str(), loader->errorString().toStdString().c_str());
            goto add_plugin_entry;
        }

        if(!instance)
        {
            LOG_WARNING("[%s] Plugin %s cannot be instantiated.", PLUGINMANAGER, path_string.c_str());
            goto add_plugin_entry;
        }

        /*-------------------------------------------------*\
        | Initialize the plugin pointer                     |
        \*-------------------------------------------------*/
        plugin                              = qobject_cast<OpenRGBPluginInterface*>(instance);

        if(!plugin)
        {
            LOG_WARNING("[%s] Plugin %s cannot be casted to OpenRGBPluginInterface", PLUGINMANAGER, path_string.c_str());
            goto add_plugin_entry;
        }

        if(plugin->GetPluginAPIVersion() != OPENRGB_PLUGIN_API_VERSION)
        {
            LOG_WARNING("[%s] Plugin %s has an incompatible API version", PLUGINMANAGER, path_string.c_str());
            goto add_plugin_entry;
        }

        /*-------------------------------------------------*\
        | Get the plugin information                        |
        \*-------------------------------------------------*/
        OpenRGBPluginInfo info              = plugin->GetPluginInfo();

        /*-------------------------------------------------*\
        | Search the settings to see if it is enabled       |
        \*-------------------------------------------------*/
        std::string     name                = "";
        std::string     description         = "";
        bool            enabled             = true;
        bool            found               = false;
        unsigned int    plugin_ct           = 0;

        if(plugin_settings.contains("plugins"))
        {
            plugin_ct = (unsigned int)plugin_settings["plugins"].size();

            for(unsigned int plugin_settings_idx = 0; plugin_settings_idx < plugin_settings["plugins"].size(); plugin_settings_idx++)
            {
                if(plugin_settings["plugins"][plugin_settings_idx].contains("name"))
                {
                    name        = plugin_settings["plugins"][plugin_settings_idx]["name"];
                }

                if(plugin_settings["plugins"][plugin_settings_idx].contains("description"))
                {
                    description = plugin_settings["plugins"][plugin_settings_idx]["description"];
                }

                if(plugin_settings["plugins"][plugin_settings_idx].contains("enabled"))
                {
                    enabled     = plugin_settings["plugins"][plugin_settings_idx]["enabled"];
                }

                if((info.Name == name)
                &&(info.Description == description))
                {
                    found = true;
                    break;
                }
            }
        }

        /*-------------------------------------------------*\
        | If the plugin was not in the list, add it to the  |
        | list and default it to enabled, then save the     |
        | settings                                          |
        \*-------------------------------------------------*/
        if(!found)
        {
            plugin_settings["plugins"][plugin_ct]["name"]           = info.Name;
            plugin_settings["plugins"][plugin_ct]["description"]    = info.Description;
            plugin_settings["plugins"][plugin_ct]["enabled"]        = enabled;

            ResourceManager::get()->GetSettingsManager()->SetSettings("Plugins", plugin_settings);
            ResourceManager::get()->GetSettingsManager()->SaveSettings();
        }

        LOG_VERBOSE("[%s] Loaded plugin %s", PLUGINMANAGER, info.Name.c_str());

        /*-----------------------------------------------------*\
        | Add the plugin to the PluginManager active plugins    |
        \*-----------------------------------------------------*/
        entry.enabled                       = enabled;
        entry.info                          = info;
        entry.incompatible                  = false;
        entry.api_version                   = plugin->GetPluginAPIVersion();
    }

add_plugin_entry:
    loader->unload();

    ActivePlugins.push_back(entry);

    if(entry.enabled)
    {
        LoadPlugin(&ActivePlugins.back());
    }
}

void PluginManager::RemovePlugin(const filesystem::path& path)
{
    unsigned int plugin_idx;

    LOG_TRACE("[%s] Attempting to remove plugin %s", PLUGINMANAGER, path.generic_u8string().c_str());

    /*-----------------------------------------------------*\
    | Search active plugins to see if this path already     |
    | exists                                                |
    \*-----------------------------------------------------*/
    for(plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(path == ActivePlugins[plugin_idx].path)
        {
            break;
        }
    }

    /*-----------------------------------------------------*\
    | If the plugin path does not exist in the active       |
    | plugins list, return                                  |
    \*-----------------------------------------------------*/
    if(plugin_idx == ActivePlugins.size())
    {
        LOG_TRACE("[%s] Plugin %s not active", PLUGINMANAGER, path.generic_u8string().c_str());
        return;
    }

    /*-----------------------------------------------------*\
    | If the selected plugin is in the list and loaded,     |
    | unload it                                             |
    \*-----------------------------------------------------*/
    if(ActivePlugins[plugin_idx].loader->isLoaded())
    {
        LOG_TRACE("[%s] Plugin %s is active, unloading", PLUGINMANAGER, path.generic_u8string().c_str());
        UnloadPlugin(&ActivePlugins[plugin_idx]);
    }

    /*-----------------------------------------------------*\
    | Remove the plugin from the active plugins list        |
    \*-----------------------------------------------------*/
    ActivePlugins.erase(ActivePlugins.begin() + plugin_idx);
}

void PluginManager::EnablePlugin(const filesystem::path& path)
{
    unsigned int plugin_idx;

    /*-----------------------------------------------------*\
    | Search active plugins to see if this path already     |
    | exists                                                |
    \*-----------------------------------------------------*/
    for(plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(path == ActivePlugins[plugin_idx].path)
        {
            break;
        }
    }

    /*-----------------------------------------------------*\
    | If the plugin path does not exist in the active       |
    | plugins list, return                                  |
    \*-----------------------------------------------------*/
    if(plugin_idx == ActivePlugins.size())
    {
        return;
    }

    ActivePlugins[plugin_idx].enabled = true;
    LoadPlugin(&ActivePlugins[plugin_idx]);
}

void PluginManager::LoadPlugin(OpenRGBPluginEntry* plugin_entry)
{
    /*-----------------------------------------------------*\
    | If the plugin is in the list but is incompatible,     |
    | return                                                |
    \*-----------------------------------------------------*/
    if(plugin_entry->incompatible)
    {
        return;
    }

    /*-----------------------------------------------------*\
    | If the selected plugin is in the list but not loaded, |
    | load it                                               |
    \*-----------------------------------------------------*/
    if(!plugin_entry->loader->isLoaded())
    {
        plugin_entry->loader->load();

        QObject* instance                = plugin_entry->loader->instance();

        if(instance)
        {
            OpenRGBPluginInterface* plugin = qobject_cast<OpenRGBPluginInterface*>(instance);

            if(plugin)
            {
                if(plugin->GetPluginAPIVersion() == OPENRGB_PLUGIN_API_VERSION)
                {
                    plugin_entry->plugin = plugin;

                    plugin->Load(plugin_entry->api);

                    /*-------------------------------------*\
                    | Call the Add Plugin callback          |
                    \*-------------------------------------*/
                    if(AddPluginCallbackArg != nullptr)
                    {
                        AddPluginCallbackVal(AddPluginCallbackArg, plugin_entry);
                    }
                }
            }
        }
    }
}

void PluginManager::DisablePlugin(const filesystem::path& path)
{
    unsigned int plugin_idx;

    /*-----------------------------------------------------*\
    | Search active plugins to see if this path already     |
    | exists                                                |
    \*-----------------------------------------------------*/
    for(plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(path == ActivePlugins[plugin_idx].path)
        {
            break;
        }
    }

    /*-----------------------------------------------------*\
    | If the plugin path does not exist in the active       |
    | plugins list, return                                  |
    \*-----------------------------------------------------*/
    if(plugin_idx == ActivePlugins.size())
    {
        return;
    }

    ActivePlugins[plugin_idx].enabled = false;
    UnloadPlugin(&ActivePlugins[plugin_idx]);
}

void PluginManager::UnloadPlugin(OpenRGBPluginEntry* plugin_entry)
{
    /*-----------------------------------------------------*\
    | If the selected plugin is in the list and loaded,     |
    | unload it                                             |
    \*-----------------------------------------------------*/
    if(plugin_entry->loader->isLoaded())
    {
        /*-------------------------------------------------*\
        | Call plugin's Unload function before GUI removal  |
        \*-------------------------------------------------*/
        plugin_entry->plugin->Unload();

        /*-------------------------------------------------*\
        | Call the Remove Plugin callback                   |
        \*-------------------------------------------------*/
        if(RemovePluginCallbackVal != nullptr)
        {
            RemovePluginCallbackVal(RemovePluginCallbackArg, plugin_entry);
        }

        bool unloaded = plugin_entry->loader->unload();

        if(!unloaded)
        {
            LOG_WARNING("[%s] Plugin %s cannot be unloaded", PLUGINMANAGER, plugin_entry->path.c_str());
        }
        else
        {
            LOG_TRACE("[%s] Plugin %s successfully unloaded", PLUGINMANAGER, plugin_entry->path.c_str());
        }
    }
    else
    {
        LOG_TRACE("[%s] Plugin %s was already unloaded", PLUGINMANAGER, plugin_entry->path.c_str());
    }
}

void PluginManager::LoadPlugins()
{
    for(OpenRGBPluginEntry& plugin_entry: ActivePlugins)
    {
        if(plugin_entry.enabled)
        {
            LoadPlugin(&plugin_entry);
        }
    }
}

void PluginManager::UnloadPlugins()
{
    for(OpenRGBPluginEntry& plugin_entry: ActivePlugins)
    {
        UnloadPlugin(&plugin_entry);
    }
}

void PluginManager::OnProfileAboutToLoad()
{
    /*-----------------------------------------------------*\
    | Loop through all plugins and signal profile about to  |
    | load                                                  |
    \*-----------------------------------------------------*/
    for(std::size_t plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            ActivePlugins[plugin_idx].plugin->OnProfileAboutToLoad();
        }
    }
}

void PluginManager::OnProfileLoad(nlohmann::json profile_data)
{
    /*-----------------------------------------------------*\
    | Loop through all plugins call their OnProfileLoad if  |
    | the profile data contains an entry for that plugin    |
    \*-----------------------------------------------------*/
    for(std::size_t plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            if(profile_data.contains(ActivePlugins[plugin_idx].plugin->GetPluginInfo().Name))
            {
                ActivePlugins[plugin_idx].plugin->OnProfileLoad(profile_data[ActivePlugins[plugin_idx].plugin->GetPluginInfo().Name]);
            }
        }
    }
}

nlohmann::json PluginManager::OnProfileSave()
{
    nlohmann::json plugin_json;

    /*-----------------------------------------------------*\
    | Loop through all plugins and gather their profile     |
    | data                                                  |
    \*-----------------------------------------------------*/
    for(std::size_t plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            plugin_json[ActivePlugins[plugin_idx].plugin->GetPluginInfo().Name] = ActivePlugins[plugin_idx].plugin->OnProfileSave();
        }
    }

    return(plugin_json);
}

nlohmann::json PluginManager::OnProfileSave(std::vector<std::string> enabled_plugins)
{
    nlohmann::json plugin_json;

    /*-----------------------------------------------------*\
    | Loop through all plugins and gather their profile     |
    | data if the plugin name is in the enabled list        |
    \*-----------------------------------------------------*/
    for(std::size_t plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            for(std::size_t enabled_plugin_idx = 0; enabled_plugin_idx < enabled_plugins.size(); enabled_plugin_idx++)
            {
                if(enabled_plugins[enabled_plugin_idx] == ActivePlugins[plugin_idx].info.Name)
                {
                    plugin_json[ActivePlugins[plugin_idx].plugin->GetPluginInfo().Name] = ActivePlugins[plugin_idx].plugin->OnProfileSave();
                    break;
                }
            }
        }
    }

    return(plugin_json);
}

unsigned char * PluginManager::OnSDKCommand(unsigned int plugin_idx, unsigned int pkt_id, unsigned char * pkt_data, unsigned int * pkt_size)
{
    unsigned char * out_data = NULL;

    if(plugin_idx < ActivePlugins.size())
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            out_data = ActivePlugins[plugin_idx].plugin->OnSDKCommand(pkt_id, pkt_data, pkt_size);
        }
    }

    return(out_data);
}

/*---------------------------------------------------------*\
| Callback functions                                        |
\*---------------------------------------------------------*/
void PluginManager::ProfileManagerCallback(unsigned int update_reason)
{
    for(std::size_t plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            ActivePlugins[plugin_idx].plugin->ProfileManagerUpdated(update_reason);
        }
    }
}

void PluginManager::ResourceManagerCallback(unsigned int update_reason)
{
    for(std::size_t plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            ActivePlugins[plugin_idx].plugin->ResourceManagerUpdated(update_reason);
        }
    }
}

void PluginManager::SettingsManagerCallback(unsigned int update_reason)
{
    for(std::size_t plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            ActivePlugins[plugin_idx].plugin->SettingsManagerUpdated(update_reason);
        }
    }
}

/*---------------------------------------------------------*\
| Plugin Information                                        |
\*---------------------------------------------------------*/
unsigned int PluginManager::GetPluginCount()
{
    return((unsigned int)ActivePlugins.size());
}

std::string PluginManager::GetPluginDescription(unsigned int plugin_idx)
{
    if(plugin_idx < ActivePlugins.size())
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            return(ActivePlugins[plugin_idx].plugin->GetPluginInfo().Description);
        }
    }

    return("");
}

std::string PluginManager::GetPluginName(unsigned int plugin_idx)
{
    if(plugin_idx < ActivePlugins.size())
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            return(ActivePlugins[plugin_idx].plugin->GetPluginInfo().Name);
        }
    }

    return("");
}

unsigned int PluginManager::GetPluginProtocolVersion(unsigned int plugin_idx)
{
    if(plugin_idx < ActivePlugins.size())
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            return(ActivePlugins[plugin_idx].plugin->GetPluginInfo().ProtocolVersion);
        }
    }

    return(0);
}

std::string PluginManager::GetPluginVersion(unsigned int plugin_idx)
{
    if(plugin_idx < ActivePlugins.size())
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            return(ActivePlugins[plugin_idx].plugin->GetPluginInfo().Version);
        }
    }

    return("");
}

/*---------------------------------------------------------*\
| Plugin-Created RGBControllers                             |
\*---------------------------------------------------------*/
std::vector<RGBController *> PluginManager::GetRGBControllers()
{
    std::vector<RGBController *> plugin_controllers;

    for(std::size_t plugin_idx = 0; plugin_idx < ActivePlugins.size(); plugin_idx++)
    {
        if(ActivePlugins[plugin_idx].enabled && ActivePlugins[plugin_idx].loader->isLoaded())
        {
            plugin_controllers.insert(plugin_controllers.begin(), ((OpenRGBPluginAPI*)ActivePlugins[plugin_idx].api)->rgb_controllers.begin(), ((OpenRGBPluginAPI*)ActivePlugins[plugin_idx].api)->rgb_controllers.end());
        }
    }

    return(plugin_controllers);
}
