/*---------------------------------------------------------*\
| NetworkServer.cpp                                         |
|                                                           |
|   OpenRGB SDK network server                              |
|                                                           |
|   Adam Honse (CalcProgrammer1)                09 May 2020 |
|                                                           |
|   This file is part of the OpenRGB project                |
|   SPDX-License-Identifier: GPL-2.0-or-later               |
\*---------------------------------------------------------*/

#include <cstring>
#include <queue>
#include "i2c_smbus.h"
#include "JsonUtils.h"
#include "LogManager.h"
#include "NetworkServer.h"
#include "StringUtils.h"

#ifndef _WIN32
#include <sys/ioctl.h>
#include <netinet/tcp.h>
#include <sys/types.h>
#include <arpa/inet.h>
#else
#include <ws2tcpip.h>
#endif
#include <memory.h>
#include <errno.h>
#include <stdlib.h>

#ifdef _WIN32
#include <Windows.h>
#define MSG_NOSIGNAL 0
#else
#include <unistd.h>
#endif

#ifdef __linux__
const int yes = 1;
#else
const char yes = 1;
#endif

using namespace std::chrono_literals;

/*---------------------------------------------------------*\
| Macros for copying data fields from set descriptor buffer |
| while ensuring we don't access out of bounds              |
\*---------------------------------------------------------*/
#define COPY_DATA_FIELD(data_ptr, data_start, field)                                \
    if((unsigned)(data_ptr + sizeof(field) - data_start) <= (unsigned)data_size)    \
    {                                                                               \
        memcpy(&field, data_ptr, sizeof(field));                                    \
        data_ptr += sizeof(field);                                                  \
    }                                                                               \
    else                                                                            \
    {                                                                               \
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);                               \
    }                                                                               \

#define COPY_STRING_FIELD(data_ptr, data_start, length, field)                      \
    if((unsigned)(data_ptr + length - data_start) <= (unsigned)data_size)           \
    {                                                                               \
        field.assign((char *)data_ptr, length);                                     \
        field = StringUtils::remove_null_terminating_chars(field);                  \
        data_ptr += length;                                                         \
    }                                                                               \
    else                                                                            \
    {                                                                               \
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);                               \
    }                                                                               \

#define COPY_DATA_FIELD_UNLOCK(data_ptr, data_start, field, controller)             \
    if((unsigned)(data_ptr + sizeof(field) - data_start) <= (unsigned)data_size)    \
    {                                                                               \
        memcpy(&field, data_ptr, sizeof(field));                                    \
        data_ptr += sizeof(field);                                                  \
    }                                                                               \
    else                                                                            \
    {                                                                               \
        controller->AccessMutex.unlock();                                           \
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);                               \
    }                                                                               \

/*---------------------------------------------------------*\
| NetworkServer name for log entries                        |
\*---------------------------------------------------------*/
const char* NETWORKSERVER = "NetworkServer";

NetworkClientInfo::NetworkClientInfo()
{
    client_string           = "Client";
    client_ip               = OPENRGB_SDK_HOST;
    client_sock             = INVALID_SOCKET;
    client_listen_thread    = nullptr;
    client_protocol_version = 0;
    client_is_local         = false;
    client_is_local_client  = false;
    client_send_thread      = nullptr;
    client_send_running     = false;
}

NetworkClientInfo::~NetworkClientInfo()
{
    if(client_sock != INVALID_SOCKET)
    {
        LOG_INFO("[%s] Closing server connection: %s", NETWORKSERVER, client_ip.c_str());

        /*-------------------------------------------------*\
        | Stop the send thread before closing the socket.   |
        | SD_BOTH (not just SD_RECEIVE) unblocks a send     |
        | stuck on a slow client, so the join cannot hang.  |
        \*-------------------------------------------------*/
        if(client_send_thread)
        {
            client_send_running = false;
            shutdown(client_sock, SD_BOTH);
            client_send_cv.notify_all();
            client_send_thread->join();
            delete client_send_thread;
            client_send_thread = nullptr;

            /*---------------------------------------------*\
            | Free any packets still queued; the entries    |
            | own their data pointers.                      |
            \*---------------------------------------------*/
            for(std::list<NetworkServerClientSendQueueEntry>::iterator entry = client_send_queue.begin(); entry != client_send_queue.end(); entry++)
            {
                delete[] entry->data;
            }

            client_send_queue.clear();
            client_send_coalesce.clear();
        }
        else
        {
            shutdown(client_sock, SD_RECEIVE);
        }

        delete client_listen_thread;
        closesocket(client_sock);
    }
}

static void RGBController_UpdateCallback(void* this_ptr, unsigned int update_reason, void* controller_ptr)
{
    NetworkServer* this_obj = (NetworkServer*)this_ptr;

    this_obj->SendRequest_RGBController_SignalUpdate((RGBController*)controller_ptr, update_reason);
}

NetworkServer::NetworkServer()
{
    host                        = OPENRGB_SDK_HOST;
    port_num                    = OPENRGB_SDK_PORT;
    server_online               = false;
    server_listening            = false;
    legacy_workaround_enabled   = false;
    controller_next_idx         = 0;
    controller_updating         = false;
    server_flags                = NET_SERVER_FLAG_SUPPORTS_RGBCONTROLLER
                                | NET_SERVER_FLAG_SUPPORTS_LOGMANAGER
                                | NET_SERVER_FLAG_SUPPORTS_PROFILEMANAGER
                                | NET_SERVER_FLAG_SUPPORTS_PLUGINMANAGER
                                | NET_SERVER_FLAG_SUPPORTS_SETTINGSMANAGER
                                | NET_SERVER_FLAG_SUPPORTS_DETECTION
                                | NET_SERVER_FLAG_SUPPORTS_DEVICE_INFO;

    for(int i = 0; i < MAXSOCK; i++)
    {
        ConnectionThread[i]     = nullptr;
    }

    plugin_manager              = nullptr;
    profile_manager             = nullptr;
    profilemanager_thread       = nullptr;
    settings_manager            = nullptr;
}

NetworkServer::~NetworkServer()
{
    StopServer();

    /*-----------------------------------------------------*\
    | Unregister the server's RGBController update handler  |
    | for each RGBController in the controllers list        |
    \*-----------------------------------------------------*/
    for(std::size_t controller_idx = 0; controller_idx < controllers.size(); controller_idx++)
    {
        controllers[controller_idx]->UnregisterUpdateCallback(this);
    }
}

/*---------------------------------------------------------*\
| Server Information functions                              |
\*---------------------------------------------------------*/
std::string NetworkServer::GetHost()
{
    return(host);
}

unsigned short NetworkServer::GetPort()
{
    return(port_num);
}

bool NetworkServer::GetOnline()
{
    return(server_online);
}

bool NetworkServer::GetListening()
{
    return(server_listening);
}

unsigned int NetworkServer::GetNumClients()
{
    return((unsigned int)ServerClients.size());
}

const char * NetworkServer::GetClientString(unsigned int client_num)
{
    const char * result;

    ServerClientsMutex.lock();

    if(client_num < ServerClients.size())
    {
        result = ServerClients[client_num]->client_string.c_str();
    }
    else
    {
        result = "";
    }

    ServerClientsMutex.unlock();

    return(result);
}

const char * NetworkServer::GetClientIP(unsigned int client_num)
{
    const char * result;

    ServerClientsMutex.lock();

    if(client_num < ServerClients.size())
    {
        result = ServerClients[client_num]->client_ip.c_str();
    }
    else
    {
        result = "";
    }

    ServerClientsMutex.unlock();

    return(result);
}

unsigned int NetworkServer::GetClientProtocolVersion(unsigned int client_num)
{
    unsigned int result;

    ServerClientsMutex.lock();

    if(client_num < ServerClients.size())
    {
        result = ServerClients[client_num]->client_protocol_version;
    }
    else
    {
        result = 0;
    }

    ServerClientsMutex.unlock();

    return(result);
}

/*---------------------------------------------------------*\
| Callback functions                                        |
\*---------------------------------------------------------*/
void NetworkServer::SignalLogManagerLoggedEntry(LogMessage& logged_entry)
{
    if(ServerClients.size() > 0)
    {
        /*-------------------------------------------------*\
        | Create data buffer for message                    |
        \*-------------------------------------------------*/
        unsigned int    data_size       = 0;
        unsigned short  filename_size   = (unsigned short)strlen(logged_entry.filename.c_str()) + 1;
        unsigned short  text_size       = (unsigned short)strlen(logged_entry.text.c_str()) + 1;

        data_size                      += sizeof(data_size);
        data_size                      += sizeof(logged_entry.level);
        data_size                      += sizeof(logged_entry.line);
        data_size                      += sizeof(logged_entry.timestamp);
        data_size                      += sizeof(filename_size);
        data_size                      += filename_size;
        data_size                      += sizeof(text_size);
        data_size                      += text_size;

        unsigned char*  data_buf        = new unsigned char[data_size];
        unsigned char*  data_ptr        = data_buf;

        memcpy(data_ptr, &data_size, sizeof(data_size));
        data_ptr += sizeof(data_size);

        memcpy(data_ptr, &logged_entry.level, sizeof(logged_entry.level));
        data_ptr += sizeof(logged_entry.level);

        memcpy(data_ptr, &logged_entry.line, sizeof(logged_entry.line));
        data_ptr += sizeof(logged_entry.line);

        memcpy(data_ptr, &logged_entry.timestamp, sizeof(logged_entry.timestamp));
        data_ptr += sizeof(logged_entry.timestamp);

        memcpy(data_ptr, &filename_size, sizeof(filename_size));
        data_ptr += sizeof(filename_size);

        memcpy(data_ptr, logged_entry.filename.c_str(), filename_size);
        data_ptr += filename_size;

        memcpy(data_ptr, &text_size, sizeof(text_size));
        data_ptr += sizeof(text_size);

        memcpy(data_ptr, logged_entry.text.c_str(), text_size);
        data_ptr += text_size;

        /*-------------------------------------------------*\
        | Send Logged Entry request for all clients         |
        \*-------------------------------------------------*/
        for(unsigned int client_idx = 0; client_idx < ServerClients.size(); client_idx++)
        {
            if(ServerClients[client_idx]->client_is_local_client)
            {
                SendRequest_LoggedEntry(ServerClients[client_idx], data_size, data_buf);
            }
        }

        delete[] data_buf;
    }
}

void NetworkServer::SignalProfileManagerUpdate(unsigned int update_reason)
{
    switch(update_reason)
    {
        case PROFILEMANAGER_UPDATE_REASON_PROFILE_LIST_UPDATED:
            SignalProfileListUpdated();
            break;

        case PROFILEMANAGER_UPDATE_REASON_ACTIVE_PROFILE_CHANGED:
            SignalActiveProfileChanged();
            break;

        case PROFILEMANAGER_UPDATE_REASON_PROFILE_ABOUT_TO_LOAD:
            ProfileManager_ProfileAboutToLoad();
            break;
    }
}

void NetworkServer::SignalResourceManagerUpdate(unsigned int update_reason)
{
    switch(update_reason)
    {
        case RESOURCEMANAGER_UPDATE_REASON_DEVICE_LIST_UPDATED:
            SignalDeviceListUpdated();
            break;

        case RESOURCEMANAGER_UPDATE_REASON_DETECTION_STARTED:
            SignalDetectionStarted();
            break;

        case RESOURCEMANAGER_UPDATE_REASON_DETECTION_PROGRESS_CHANGED:
            SignalDetectionProgress();
            break;

        case RESOURCEMANAGER_UPDATE_REASON_DETECTION_COMPLETE:
            SignalDetectionCompleted();
            break;
    }
}

void NetworkServer::RegisterClientInfoChangeCallback(NetServerCallback new_callback, void* new_callback_arg)
{
    ClientInfoChangeCallbacks.push_back(new_callback);
    ClientInfoChangeCallbackArgs.push_back(new_callback_arg);
}

void NetworkServer::RegisterServerListeningChangeCallback(NetServerCallback new_callback, void* new_callback_arg)
{
    ServerListeningChangeCallbacks.push_back(new_callback);
    ServerListeningChangeCallbackArgs.push_back(new_callback_arg);
}

/*---------------------------------------------------------*\
| Server Configuration functions                            |
\*---------------------------------------------------------*/
void NetworkServer::SetHost(std::string new_host)
{
    if(server_online == false)
    {
        host = new_host;
    }
}

void NetworkServer::SetLegacyWorkaroundEnable(bool enable)
{
    legacy_workaround_enabled = enable;
}

void NetworkServer::SetName(std::string new_name)
{
    /*-----------------------------------------------------*\
    | Store the server name                                 |
    \*-----------------------------------------------------*/
    server_name = new_name;

    /*-----------------------------------------------------*\
    | Send server name to all clients                       |
    \*-----------------------------------------------------*/
    for(std::size_t client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        SendReply_ServerString(ServerClients[client_idx]);
    }
}

void NetworkServer::SetPort(unsigned short new_port)
{
    if(server_online == false)
    {
        port_num = new_port;
    }
}

/*---------------------------------------------------------*\
| Server Control functions                                  |
\*---------------------------------------------------------*/
void NetworkServer::StartServer()
{
    int err;
    struct addrinfo hints, *res, *result;

    /*-----------------------------------------------------*\
    | Start a TCP server and launch threads                 |
    \*-----------------------------------------------------*/
    char port_str[6];
    snprintf(port_str, 6, "%d", port_num);

    socket_count = 0;

    /*-----------------------------------------------------*\
    | Windows requires WSAStartup before using sockets      |
    \*-----------------------------------------------------*/
#ifdef WIN32
    if(WSAStartup(MAKEWORD(2, 2), &wsa) != NO_ERROR)
    {
        WSACleanup();
        return;
    }
#endif

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    err = getaddrinfo(host.c_str(), port_str, &hints, &result);

    if(err)
    {
        LOG_ERROR("[%s] Unable to get address.", NETWORKSERVER);
        WSACleanup();
        return;
    }

    /*-----------------------------------------------------*\
    | Create a server socket for each address returned.     |
    \*-----------------------------------------------------*/
    for(res = result; res && socket_count < MAXSOCK; res = res->ai_next)
    {
        server_sock[socket_count] = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

        if(server_sock[socket_count] == INVALID_SOCKET)
        {
            LOG_ERROR("[%s] Network socket could not be created.", NETWORKSERVER);
            WSACleanup();
            return;
        }

        /*-------------------------------------------------*\
        | Set socket options - reuse addr                   |
        \*-------------------------------------------------*/
        setsockopt(server_sock[socket_count], SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));

        /*-------------------------------------------------*\
        | Bind the server socket                            |
        \*-------------------------------------------------*/
        if(bind(server_sock[socket_count], res->ai_addr, res->ai_addrlen) == SOCKET_ERROR)
        {
            if(errno == EADDRINUSE)
            {
                LOG_ERROR("[%s] Could not bind network socket. Is port %hu already being used?", NETWORKSERVER, GetPort());
            }
            else if(errno == EACCES)
            {
                LOG_ERROR("[%s] Could not bind network socket. Access to socket was denied.", NETWORKSERVER);
            }
            else if(errno == EBADF)
            {
                LOG_ERROR("[%s] Could not bind network socket. sockfd is not a valid file descriptor.", NETWORKSERVER);
            }
            else if(errno == EINVAL)
            {
                LOG_ERROR("[%s] Could not bind network socket. The socket is already bound to an address, or addrlen is wrong, or addr is not a valid address for this socket's domain.", NETWORKSERVER);
            }
            else if(errno == ENOTSOCK)
            {
                LOG_ERROR("[%s] Could not bind network socket. The file descriptor sockfd does not refer to a socket.", NETWORKSERVER);
            }
            else
            {
                /*---------------------------------------------------------*\
                | errno could be a Linux specific error, see:               |
                | https://man7.org/linux/man-pages/man2/bind.2.html         |
                \*---------------------------------------------------------*/
                LOG_ERROR("[%s] Could not bind network socket. Error code: %d.", NETWORKSERVER, errno);
            }

            WSACleanup();
            return;
        }

        /*-------------------------------------------------*\
        | Set socket options - no delay                     |
        \*-------------------------------------------------*/
        setsockopt(server_sock[socket_count], IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes));

        socket_count += 1;
    }

    freeaddrinfo(result);
    server_online = true;

    /*-----------------------------------------------------*\
    | Start the ProfileManager thread                       |
    \*-----------------------------------------------------*/
    profilemanager_thread                           = new NetworkServerControllerThread;

    profilemanager_thread->id                       = 0;
    profilemanager_thread->index                    = 0;
    profilemanager_thread->online                   = true;
    profilemanager_thread->thread                   = new std::thread(&NetworkServer::ProfileManagerListenThread, this, profilemanager_thread);

    /*-----------------------------------------------------*\
    | Start the connection thread                           |
    \*-----------------------------------------------------*/
    for(int curr_socket = 0; curr_socket < socket_count; curr_socket++)
    {
        ConnectionThread[curr_socket] = new std::thread(&NetworkServer::ConnectionThreadFunction, this, curr_socket);
        ConnectionThread[curr_socket]->detach();
    }
}

void NetworkServer::StopServer()
{
    int curr_socket;
    server_online = false;

    ServerClientsMutex.lock();

    for(unsigned int client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        delete ServerClients[client_idx];
    }

    ServerClients.clear();

    for(curr_socket = 0; curr_socket < socket_count; curr_socket++)
    {
        shutdown(server_sock[curr_socket], SD_RECEIVE);
        closesocket(server_sock[curr_socket]);
    }

    ServerClientsMutex.unlock();

    for(curr_socket = 0; curr_socket < socket_count; curr_socket++)
    {
        if(ConnectionThread[curr_socket])
        {
            delete ConnectionThread[curr_socket];
            ConnectionThread[curr_socket] = nullptr;
        }
    }

    socket_count = 0;

    /*-----------------------------------------------------*\
    | Close the ProfileManager listen thread                |
    \*-----------------------------------------------------*/
    if(profilemanager_thread)
    {
        profilemanager_thread->online = false;
        profilemanager_thread->start_cv.notify_all();
        profilemanager_thread->thread->join();
        delete profilemanager_thread->thread;
        delete profilemanager_thread;
        profilemanager_thread = nullptr;
    }

    /*-----------------------------------------------------*\
    | Client info has changed, call the callbacks           |
    \*-----------------------------------------------------*/
    SignalClientInfoChanged();
}

/*---------------------------------------------------------*\
| Server Interface functions                                |
\*---------------------------------------------------------*/
void NetworkServer::SetControllers(std::vector<RGBController *> new_controllers)
{
    /*-----------------------------------------------------*\
    | Set the controller list updating flag to pause the    |
    | controller packet processing                          |
    \*-----------------------------------------------------*/
    controller_updating = true;

    /*-----------------------------------------------------*\
    | Lock the controller list mutex                        |
    \*-----------------------------------------------------*/
    controller_ids_mutex.lock();

    /*-----------------------------------------------------*\
    | Update the controllers list                           |
    \*-----------------------------------------------------*/
    controllers = new_controllers;

    /*-----------------------------------------------------*\
    | Create a copy of the current controller IDs list      |
    \*-----------------------------------------------------*/
    std::vector<NetworkControllerID> controller_ids_old  = controller_ids;

    /*-----------------------------------------------------*\
    | Clear the current controller IDs list                 |
    \*-----------------------------------------------------*/
    controller_ids.clear();

    /*-----------------------------------------------------*\
    | Resize controller IDs list to be the same size as the |
    | controllers list                                      |
    \*-----------------------------------------------------*/
    controller_ids.resize(controllers.size());

    /*-----------------------------------------------------*\
    | Loop through the controllers list and find the ID for |
    | each controller, adding it to the IDs list.           |
    \*-----------------------------------------------------*/
    for(std::size_t controller_idx = 0; controller_idx < controller_ids.size(); controller_idx++)
    {
        bool match                                          = false;
        for(std::size_t controller_id_idx = 0; controller_id_idx < controller_ids_old.size(); controller_id_idx++)
        {
            if(controllers[controller_idx] == controller_ids_old[controller_id_idx].controller)
            {
                controller_ids[controller_idx]              = controller_ids_old[controller_id_idx];
                match                                       = true;

                break;
            }
        }

        /*-------------------------------------------------*\
        | If an ID does not already exist, create a new ID  |
        | for this controller                               |
        \*-------------------------------------------------*/
        if(!match)
        {
            NetworkControllerID new_controller_id;

            new_controller_id.controller                    = controllers[controller_idx];
            new_controller_id.id                            = controller_next_idx;

            controller_next_idx++;

            controller_ids[controller_idx]                  = new_controller_id;
        }
    }

    /*-----------------------------------------------------*\
    | Lock the controller threads mutex                     |
    \*-----------------------------------------------------*/
    controller_threads_mutex.lock();

    /*-----------------------------------------------------*\
    | Create a copy of the current controller threads list  |
    \*-----------------------------------------------------*/
    std::vector<NetworkServerControllerThread *> controller_threads_old = controller_threads;

    /*-----------------------------------------------------*\
    | Clear the current controller threads list             |
    \*-----------------------------------------------------*/
    controller_threads.clear();

    /*-----------------------------------------------------*\
    | Resize the controller threads so that there is one    |
    | thread per controller ID                              |
    \*-----------------------------------------------------*/
    controller_threads.resize(controller_ids.size());

    /*-----------------------------------------------------*\
    | Loop through the controller IDs list and find the     |
    | thread for each ID, adding it to the threads list.    |
    \*-----------------------------------------------------*/
    for(std::size_t controller_id_idx = 0; controller_id_idx < controller_ids.size(); controller_id_idx++)
    {
        std::size_t controller_thread_old_idx   = 0;
        bool        match                       = false;

        for(; controller_thread_old_idx < controller_threads_old.size(); controller_thread_old_idx++)
        {
            if(controller_ids[controller_id_idx].id == controller_threads_old[controller_thread_old_idx]->id)
            {
                match = true;

                break;
            }
        }

        /*-----------------------------------------------------*\
        | If an existing thread was found with this ID, copy it |
        | into the new list at the new index.                   |
        \*-----------------------------------------------------*/
        if(match)
        {
            controller_threads[controller_id_idx]           = controller_threads_old[controller_thread_old_idx];
            controller_threads[controller_id_idx]->index    = (unsigned int)controller_id_idx;

            controller_threads_old.erase(controller_threads_old.begin() + controller_thread_old_idx);
        }
        /*-----------------------------------------------------*\
        | Otherwise, if an existing thread was not found with   |
        |this ID, create a new one.                             |
        \*-----------------------------------------------------*/
        else
        {
            NetworkServerControllerThread * new_controller_thread = new NetworkServerControllerThread;

            new_controller_thread->id                       = controller_ids[controller_id_idx].id;
            new_controller_thread->index                    = (unsigned int)controller_id_idx;
            new_controller_thread->online                   = true;
            new_controller_thread->thread                   = new std::thread(&NetworkServer::ControllerListenThread, this, new_controller_thread);

            controller_threads[controller_id_idx] = new_controller_thread;
        }
    }

    /*-----------------------------------------------------*\
    | Unlock the controller IDs mutex                       |
    \*-----------------------------------------------------*/
    controller_ids_mutex.unlock();

    /*-----------------------------------------------------*\
    | Loop through the remaining threads in the old list    |
    | and shut them down, their controller IDs are no       |
    | longer valid.                                         |
    \*-----------------------------------------------------*/
    for(std::size_t controller_thread_old_idx = 0; controller_thread_old_idx < controller_threads_old.size(); controller_thread_old_idx++)
    {
        controller_threads_old[controller_thread_old_idx]->online   = false;
        controller_threads_old[controller_thread_old_idx]->start_cv.notify_all();
        controller_threads_old[controller_thread_old_idx]->thread->join();
        delete controller_threads_old[controller_thread_old_idx]->thread;
    }

    /*-----------------------------------------------------*\
    | Unlock the controller threads mutex                   |
    \*-----------------------------------------------------*/
    controller_threads_mutex.unlock();

    /*-----------------------------------------------------*\
    | Register the server's RGBController update handler    |
    | for each RGBController in the controllers list        |
    \*-----------------------------------------------------*/
    for(std::size_t controller_idx = 0; controller_idx < controllers.size(); controller_idx++)
    {
        controllers[controller_idx]->RegisterUpdateCallback(RGBController_UpdateCallback, this);
    }

    /*-----------------------------------------------------*\
    | Clear the controller list updating flag to resume the |
    | controller packet processing                          |
    \*-----------------------------------------------------*/
    controller_updating = false;
}

void NetworkServer::SetPluginManager(PluginManagerInterface* plugin_manager_pointer)
{
    plugin_manager = plugin_manager_pointer;
}

void NetworkServer::SetProfileManager(ProfileManagerInterface* profile_manager_pointer)
{
    profile_manager = profile_manager_pointer;
}

void NetworkServer::SetSettingsManager(SettingsManagerInterface* settings_manager_pointer)
{
    settings_manager = settings_manager_pointer;
}

/*---------------------------------------------------------*\
| Server callback signal functions                          |
\*---------------------------------------------------------*/
void NetworkServer::SignalActiveProfileChanged()
{
    if(profile_manager)
    {
        std::string active_profile = profile_manager->GetActiveProfile();

        /*-------------------------------------------------*\
        | Indicate to the clients that the profile list has |
        | changed                                           |
        \*-------------------------------------------------*/
        for(unsigned int client_idx = 0; client_idx < ServerClients.size(); client_idx++)
        {
            if(ServerClients[client_idx]->client_flags & NET_CLIENT_FLAG_SUPPORTS_PROFILEMANAGER)
            {
                SendRequest_ProfileManager_ActiveProfileChanged(ServerClients[client_idx], active_profile);
            }
        }
    }
}

void NetworkServer::SignalClientInfoChanged()
{
    ClientInfoChangeMutex.lock();

    /*-----------------------------------------------------*\
    | Client info has changed, call the callbacks           |
    \*-----------------------------------------------------*/
    for(unsigned int callback_idx = 0; callback_idx < ClientInfoChangeCallbacks.size(); callback_idx++)
    {
        ClientInfoChangeCallbacks[callback_idx](ClientInfoChangeCallbackArgs[callback_idx]);
    }

    ClientInfoChangeMutex.unlock();
}

void NetworkServer::SignalDetectionCompleted()
{
    /*-----------------------------------------------------*\
    | Indicate to the clients that detection has completed  |
    \*-----------------------------------------------------*/
    for(unsigned int client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        SendRequest_DetectionCompleted(ServerClients[client_idx]);
    }
}

void NetworkServer::SignalDetectionProgress()
{
    unsigned int detection_percent = ResourceManager::get()->GetDetectionPercent();
    std::string  detection_string  = ResourceManager::get()->GetDetectionString();

    /*-----------------------------------------------------*\
    | Indicate to the clients detection progress changed    |
    \*-----------------------------------------------------*/
    for(unsigned int client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        SendRequest_DetectionProgress(ServerClients[client_idx], detection_percent, detection_string);
    }
}

void NetworkServer::SignalDetectionStarted()
{
    /*-----------------------------------------------------*\
    | Indicate to the clients that detection has started    |
    \*-----------------------------------------------------*/
    for(unsigned int client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        SendRequest_DetectionStarted(ServerClients[client_idx]);
    }
}

void NetworkServer::SignalDeviceListUpdated()
{
    /*-----------------------------------------------------*\
    | Indicate to the clients that the controller list has  |
    | changed                                               |
    \*-----------------------------------------------------*/
    for(unsigned int client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        SendRequest_DeviceListChanged(ServerClients[client_idx]);
    }
}

void NetworkServer::SignalProfileListUpdated()
{
    if(profile_manager)
    {
        unsigned char* profile_list_description = profile_manager->GetProfileListDescription();

        /*-------------------------------------------------*\
        | Indicate to the clients that the profile list has |
        | changed                                           |
        \*-------------------------------------------------*/
        for(unsigned int client_idx = 0; client_idx < ServerClients.size(); client_idx++)
        {
            SendRequest_ProfileManager_ProfileListChanged(ServerClients[client_idx], profile_list_description);
        }

        delete[] profile_list_description;
    }
}

void NetworkServer::SignalServerListeningChanged()
{
    ServerListeningChangeMutex.lock();

    /*-----------------------------------------------------*\
    | Server state has changed, call the callbacks          |
    \*-----------------------------------------------------*/
    for(unsigned int callback_idx = 0; callback_idx < ServerListeningChangeCallbacks.size(); callback_idx++)
    {
        ServerListeningChangeCallbacks[callback_idx](ServerListeningChangeCallbackArgs[callback_idx]);
    }

    ServerListeningChangeMutex.unlock();
}

/*---------------------------------------------------------*\
| Server Thread functions                                   |
\*---------------------------------------------------------*/
void NetworkServer::ConnectionThreadFunction(int socket_idx)
{
    /*-----------------------------------------------------*\
    | This thread handles client connections                |
    \*-----------------------------------------------------*/
    LOG_INFO("[%s] Network connection thread started on port %hu", NETWORKSERVER, GetPort());

    while(server_online == true)
    {
        /*-------------------------------------------------*\
        | Create new socket for client connection           |
        \*-------------------------------------------------*/
        NetworkClientInfo * client_info = new NetworkClientInfo();

        /*---------------------------------------------------------*\
        | Listen for incoming client connection on the server       |
        | socket.  This call blocks until a connection is           |
        | established                                               |
        \*---------------------------------------------------------*/
        if(listen(server_sock[socket_idx], 10) < 0)
        {
            LOG_INFO("[%s] Connection thread closed", NETWORKSERVER);
            server_online = false;

            return;
        }

        server_listening = true;
        SignalServerListeningChanged();

        /*-------------------------------------------------*\
        | Accept the client connection                      |
        \*-------------------------------------------------*/
        client_info->client_sock = accept_select((int)server_sock[socket_idx]);

        if(client_info->client_sock < 0)
        {
            LOG_INFO("[%s] Connection thread closed", NETWORKSERVER);
            server_online = false;

            server_listening = false;
            SignalServerListeningChanged();

            return;
        }

        /*---------------------------------------------------------*\
        | Get the new client socket and store it in the clients     |
        | vector                                                    |
        \*---------------------------------------------------------*/
        u_long arg = 0;
        ioctlsocket(client_info->client_sock, FIONBIO, &arg);
        setsockopt(client_info->client_sock, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes));

        /*-------------------------------------------------*\
        | Discover the remote hosts IP                      |
        \*-------------------------------------------------*/
        struct sockaddr_storage tmp_addr;
        char ipstr[INET6_ADDRSTRLEN];
        socklen_t len;
        len = sizeof(tmp_addr);
        getpeername(client_info->client_sock, (struct sockaddr*)&tmp_addr, &len);

        if(tmp_addr.ss_family == AF_INET)
        {
            struct sockaddr_in *s_4 = (struct sockaddr_in *)&tmp_addr;
            inet_ntop(AF_INET, &s_4->sin_addr, ipstr, sizeof(ipstr));
            client_info->client_ip = ipstr;

            if(s_4->sin_addr.s_addr == htonl(INADDR_LOOPBACK))
            {
                client_info->client_is_local = true;
            }
        }
        else
        {
            struct sockaddr_in6 *s_6 = (struct sockaddr_in6 *)&tmp_addr;
            inet_ntop(AF_INET6, &s_6->sin6_addr, ipstr, sizeof(ipstr));
            client_info->client_ip = ipstr;

            if(IN6_IS_ADDR_LOOPBACK(&s_6->sin6_addr))
            {
                client_info->client_is_local = true;
            }
        }

        /*---------------------------------------------------------*\
        | We need to lock before the thread could possibly finish   |
        \*---------------------------------------------------------*/
        ServerClientsMutex.lock();

        /*---------------------------------------------------------*\
        | Start a listener thread for the new client socket         |
        \*---------------------------------------------------------*/
        client_info->client_listen_thread = new std::thread(&NetworkServer::ListenThreadFunction, this, client_info);
        client_info->client_listen_thread->detach();

        /*-------------------------------------------------*\
        | Start the per-client send thread. It is joinable  |
        | (not detached) so the client destructor can stop  |
        | and join it cleanly.                              |
        \*-------------------------------------------------*/
        client_info->client_send_running = true;
        client_info->client_send_thread  = new std::thread(&NetworkServer::ClientSendThreadFunction, this, client_info);

        ServerClients.push_back(client_info);
        ServerClientsMutex.unlock();

        /*-------------------------------------------------*\
        | Client info has changed, call the callbacks       |
        \*-------------------------------------------------*/
        SignalClientInfoChanged();
    }

    LOG_INFO("[%s] Connection thread closed", NETWORKSERVER);
    server_online = false;
    server_listening = false;
    SignalServerListeningChanged();
}

void NetworkServer::ControllerListenThread(NetworkServerControllerThread* this_thread)
{
    while(this_thread->online == true)
    {
        /*-------------------------------------------------*\
        | Stop processing RGBController packet queues if    |
        | the controller list is being updated              |
        \*-------------------------------------------------*/
        if(!controller_updating)
        {
            std::unique_lock<std::mutex> start_lock(this_thread->start_mutex);
            this_thread->start_cv.wait(start_lock);

            while(this_thread->queue.size() > 0)
            {
                NetworkServerControllerThreadQueueEntry queue_entry;
                NetPacketStatus                         status      = NET_PACKET_STATUS_OK;

                this_thread->queue_mutex.lock();
                queue_entry = this_thread->queue.front();
                this_thread->queue.pop();
                this_thread->queue_mutex.unlock();

                controller_ids_mutex.lock_shared();

                switch(queue_entry.header.pkt_id)
                {
                    case NET_PACKET_ID_RGBCONTROLLER_UPDATELEDS:
                        status = ProcessRequest_RGBController_UpdateLEDs(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data, this_thread->id);
                        break;

                    case NET_PACKET_ID_RGBCONTROLLER_UPDATEZONELEDS:
                        status = ProcessRequest_RGBController_UpdateZoneLEDs(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data, this_thread->id);
                        break;

                    case NET_PACKET_ID_RGBCONTROLLER_UPDATEMODE:
                        status = ProcessRequest_RGBController_UpdateSaveMode(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data, this_thread->id, false);
                        break;

                    case NET_PACKET_ID_RGBCONTROLLER_SAVEMODE:
                        status = ProcessRequest_RGBController_UpdateSaveMode(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data, this_thread->id, true);
                        break;

                    case NET_PACKET_ID_RGBCONTROLLER_UPDATEZONEMODE:
                        status = ProcessRequest_RGBController_UpdateZoneMode(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data, this_thread->id);
                        break;

                    default:
                        status = NET_PACKET_STATUS_ERROR_UNSUPPORTED;
                        break;
                }

                controller_ids_mutex.unlock_shared();

                delete[] queue_entry.data;

                SendAck(queue_entry.client_info, queue_entry.header.pkt_dev_id, queue_entry.header.pkt_id, status);
            }
        }
        else
        {
            std::this_thread::sleep_for(1ms);
        }
    }
}

void NetworkServer::ProfileManagerListenThread(NetworkServerControllerThread* this_thread)
{
    while(this_thread->online == true)
    {
        std::unique_lock<std::mutex> start_lock(this_thread->start_mutex);
        this_thread->start_cv.wait(start_lock);

        while(this_thread->queue.size() > 0)
        {
            NetworkServerControllerThreadQueueEntry queue_entry;
            NetPacketStatus                         status      = NET_PACKET_STATUS_OK;

            this_thread->queue_mutex.lock();
            queue_entry = this_thread->queue.front();
            this_thread->queue.pop();
            this_thread->queue_mutex.unlock();

            switch(queue_entry.header.pkt_id)
            {
                case NET_PACKET_ID_PROFILEMANAGER_GET_PROFILE_LIST:
                    status = ProcessRequest_ProfileManager_GetProfileList(queue_entry.client_info);
                    break;

                case NET_PACKET_ID_PROFILEMANAGER_SAVE_PROFILE:
                    status = ProcessRequest_ProfileManager_SaveProfile(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data);
                    break;

                case NET_PACKET_ID_PROFILEMANAGER_LOAD_PROFILE:
                    status = ProcessRequest_ProfileManager_LoadProfile(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data);
                    break;

                case NET_PACKET_ID_PROFILEMANAGER_DELETE_PROFILE:
                    status = ProcessRequest_ProfileManager_DeleteProfile(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data);
                    break;

                case NET_PACKET_ID_PROFILEMANAGER_UPLOAD_PROFILE:
                    status = ProcessRequest_ProfileManager_UploadProfile(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data);
                    break;

                case NET_PACKET_ID_PROFILEMANAGER_DOWNLOAD_PROFILE:
                    status = ProcessRequest_ProfileManager_DownloadProfile(queue_entry.client_info, queue_entry.header.pkt_size, queue_entry.data);
                    break;

                case NET_PACKET_ID_PROFILEMANAGER_GET_ACTIVE_PROFILE:
                    status = ProcessRequest_ProfileManager_GetActiveProfile(queue_entry.client_info);
                    break;

                case NET_PACKET_ID_PROFILEMANAGER_CLEAR_ACTIVE_PROFILE:
                    status = ProcessRequest_ProfileManager_ClearActiveProfile(queue_entry.client_info);
                    break;

                default:
                    status = NET_PACKET_STATUS_ERROR_UNSUPPORTED;
                    break;
            }

            delete[] queue_entry.data;

            SendAck(queue_entry.client_info, queue_entry.header.pkt_dev_id, queue_entry.header.pkt_id, status);
        }
    }
}

void NetworkServer::ListenThreadFunction(NetworkClientInfo* client_info)
{
    SOCKET client_sock = client_info->client_sock;

    LOG_INFO("[%s] Network server started", NETWORKSERVER);

    /*-----------------------------------------------------*\
    | This thread handles messages received from clients    |
    \*-----------------------------------------------------*/
    while(server_online == true)
    {
        NetPacketHeader header;
        int             bytes_read  = 0;
        unsigned char*  data        = NULL;
        bool            delete_data = true;
        NetPacketStatus status      = NET_PACKET_STATUS_OK;

        for(unsigned int i = 0; i < 4; i++)
        {
            /*---------------------------------------------*\
            | Read byte of magic                            |
            \*---------------------------------------------*/
            bytes_read = recv_select(client_sock, &header.pkt_magic[i], 1, 0);

            if(bytes_read <= 0)
            {
                LOG_ERROR("[%s] recv_select failed receiving magic, closing listener", NETWORKSERVER);
                goto listen_done;
            }

            /*---------------------------------------------*\
            | Test characters of magic "ORGB"               |
            \*---------------------------------------------*/
            if(header.pkt_magic[i] != openrgb_sdk_magic[i])
            {
                LOG_ERROR("[%s] Invalid magic received, closing listener", NETWORKSERVER);
                goto listen_done;
            }
        }

        /*-------------------------------------------------*\
        | If we get to this point, the magic is correct.    |
        | Read the rest of the header                       |
        \*-------------------------------------------------*/
        bytes_read = 0;
        do
        {
            int tmp_bytes_read = 0;

            tmp_bytes_read = recv_select(client_sock, (char *)&header.pkt_dev_id + bytes_read, sizeof(header) - sizeof(header.pkt_magic) - bytes_read, 0);

            bytes_read += tmp_bytes_read;

            if(tmp_bytes_read <= 0)
            {
                LOG_ERROR("[%s] recv_select failed receiving header, closing listener", NETWORKSERVER);
                goto listen_done;
            }

        } while(bytes_read != sizeof(header) - sizeof(header.pkt_magic));

        /*-------------------------------------------------*\
        | Header received, now receive the data             |
        \*-------------------------------------------------*/
        bytes_read = 0;
        if(header.pkt_size > 0)
        {
            data = new unsigned char[header.pkt_size];

            do
            {
                int tmp_bytes_read = 0;

                tmp_bytes_read = recv_select(client_sock, (char*)&data[(unsigned int)bytes_read], header.pkt_size - bytes_read, 0);

                if(tmp_bytes_read <= 0)
                {
                    LOG_ERROR("[%s] recv_select failed receiving data, closing listener", NETWORKSERVER);
                    goto listen_done;
                }
                bytes_read += tmp_bytes_read;

            } while ((unsigned int)bytes_read < header.pkt_size);
        }

        /*-------------------------------------------------*\
        | Entire request received, select functionality     |
        | based on request ID                               |
        \*-------------------------------------------------*/
        switch(header.pkt_id)
        {
        /*-------------------------------------------------*\
        | Network requests                                  |
        \*-------------------------------------------------*/
            case NET_PACKET_ID_REQUEST_CONTROLLER_COUNT:
                SendReply_ControllerCount(client_info);
                break;

            case NET_PACKET_ID_REQUEST_CONTROLLER_DATA:
                {
                    /*-------------------------------------*\
                    | Protocol versions < 6 sent the client |
                    | protocol version with this request,   |
                    | versions 6+ use the stored negotiated |
                    | protocol version instead.             |
                    \*-------------------------------------*/
                    if(client_info->client_protocol_version < 6)
                    {
                        unsigned int protocol_version = 0;

                        if(header.pkt_size == sizeof(unsigned int))
                        {
                            memcpy(&protocol_version, data, sizeof(unsigned int));
                        }

                        SendReply_ControllerData(client_info, header.pkt_dev_id, protocol_version);
                    }
                    else
                    {
                        SendReply_ControllerData(client_info, header.pkt_dev_id, client_info->client_protocol_version);
                    }
                }
                break;

            case NET_PACKET_ID_REQUEST_PROTOCOL_VERSION:
                status = ProcessRequest_ClientProtocolVersion(client_info, header.pkt_size, data);
                SendReply_ProtocolVersion(client_info);
                SendReply_ServerString(client_info);
                break;

            case NET_PACKET_ID_SET_CLIENT_FLAGS:
                status = ProcessRequest_ClientFlags(client_info, header.pkt_size, data);
                SendReply_ServerFlags(client_info);
                break;

            case NET_PACKET_ID_SET_CLIENT_NAME:
                status = ProcessRequest_ClientString(client_info, header.pkt_size, data);
                break;

            case NET_PACKET_ID_REQUEST_RESCAN_DEVICES:
                status = ProcessRequest_RescanDevices();
                break;

            case NET_PACKET_ID_GET_I2C_BUS_INFO:
                status = ProcessRequest_GetI2CBusInfo(client_info);
                break;

            case NET_PACKET_ID_GET_HID_DEVICE_INFO:
                status = ProcessRequest_GetHIDDeviceInfo(client_info);
                break;

            case NET_PACKET_ID_GET_USB_DEVICE_INFO:
                status = ProcessRequest_GetUSBDeviceInfo(client_info);
                break;

            case NET_PACKET_ID_GET_SERIAL_PORTS:
                status = ProcessRequest_GetSerialPorts(client_info);
                break;

            case NET_PACKET_ID_GET_USB_SERIAL_PORTS:
                status = ProcessRequest_GetUSBSerialPorts(client_info);
                break;

        /*-------------------------------------------------*\
        | LogManager functions                              |
        \*-------------------------------------------------*/
            case NET_PACKET_ID_LOGMANAGER_CLEAR_LOG_BUFFER:
                status = ProcessRequest_LogManager_ClearLogBuffer(client_info);
                break;

            case NET_PACKET_ID_LOGMANAGER_GET_LOG_BUFFER:
                status = ProcessRequest_LogManager_GetLogBuffer(client_info);
                break;

            case NET_PACKET_ID_LOGMANAGER_GET_LOG_LEVEL:
                status = ProcessRequest_LogManager_GetLogLevel(client_info);
                break;

            case NET_PACKET_ID_LOGMANAGER_SET_LOG_LEVEL:
                status = ProcessRequest_LogManager_SetLogLevel(client_info, header.pkt_size, data);
                break;

        /*-------------------------------------------------*\
        | ProfileManager functions are handled in a         |
        | separate thread, queue the messages               |
        \*-------------------------------------------------*/
            case NET_PACKET_ID_PROFILEMANAGER_GET_PROFILE_LIST:
            case NET_PACKET_ID_PROFILEMANAGER_SAVE_PROFILE:
            case NET_PACKET_ID_PROFILEMANAGER_LOAD_PROFILE:
            case NET_PACKET_ID_PROFILEMANAGER_DELETE_PROFILE:
            case NET_PACKET_ID_PROFILEMANAGER_UPLOAD_PROFILE:
            case NET_PACKET_ID_PROFILEMANAGER_DOWNLOAD_PROFILE:
            case NET_PACKET_ID_PROFILEMANAGER_GET_ACTIVE_PROFILE:
            case NET_PACKET_ID_PROFILEMANAGER_CLEAR_ACTIVE_PROFILE:
                {
                    profilemanager_thread->queue_mutex.lock();

                    NetworkServerControllerThreadQueueEntry new_entry;
                    new_entry.data                      = data;
                    new_entry.header                    = header;
                    new_entry.client_info               = client_info;

                    profilemanager_thread->queue.push(new_entry);
                    profilemanager_thread->queue_mutex.unlock();
                    profilemanager_thread->start_cv.notify_all();

                    delete_data = false;
                }
                break;

            case NET_PACKET_ID_PROFILEMANAGER_PROFILE_ABOUT_TO_LOAD:
                profile_about_to_load_acks++;
                break;

        /*-------------------------------------------------*\
        | PluginManager functions                           |
        \*-------------------------------------------------*/
            case NET_PACKET_ID_PLUGINMANAGER_GET_PLUGIN_LIST:
                SendReply_PluginList(client_info);
                break;

            case NET_PACKET_ID_PLUGINMANAGER_PLUGIN_SPECIFIC:
                if(plugin_manager)
                {
                    unsigned int    plugin_pkt_id   = *((unsigned int*)(data));
                    unsigned int    plugin_pkt_size = header.pkt_size - (sizeof(unsigned int));
                    unsigned char*  plugin_data     = (data + sizeof(unsigned int));
                    unsigned char*  output          = plugin_manager->OnSDKCommand(header.pkt_dev_id, plugin_pkt_id, plugin_data, &plugin_pkt_size);

                    if(output != nullptr)
                    {
                        SendReply_PluginSpecific(client_info, plugin_pkt_size, output, plugin_pkt_id);
                    }
                }
                break;

        /*-------------------------------------------------*\
        | SettingsManager functions                         |
        \*-------------------------------------------------*/
            case NET_PACKET_ID_SETTINGSMANAGER_GET_SETTINGS:
                status = ProcessRequest_SettingsManager_GetSettings(client_info, header.pkt_size, data);
                break;

            case NET_PACKET_ID_SETTINGSMANAGER_GET_SETTINGS_SCHEMA:
                status = ProcessRequest_SettingsManager_GetSettingsSchema(client_info, header.pkt_size, data);
                break;

            case NET_PACKET_ID_SETTINGSMANAGER_MODIFY_SETTINGS:
                status = ProcessRequest_SettingsManager_ModifySettings(client_info, header.pkt_size, data);
                break;

            case NET_PACKET_ID_SETTINGSMANAGER_SET_SETTINGS:
                status = ProcessRequest_SettingsManager_SetSettings(client_info, header.pkt_size, data);
                break;

            case NET_PACKET_ID_SETTINGSMANAGER_SAVE_SETTINGS:
                status = ProcessRequest_SettingsManager_SaveSettings(client_info);
                break;

        /*-------------------------------------------------*\
        | RGBController functions                           |
        \*-------------------------------------------------*/
            case NET_PACKET_ID_RGBCONTROLLER_RESIZEZONE:
                status = ProcessRequest_RGBController_ResizeZone(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_UPDATELEDS:
            case NET_PACKET_ID_RGBCONTROLLER_UPDATEZONELEDS:
            case NET_PACKET_ID_RGBCONTROLLER_UPDATEMODE:
            case NET_PACKET_ID_RGBCONTROLLER_SAVEMODE:
            case NET_PACKET_ID_RGBCONTROLLER_UPDATEZONEMODE:
                {
                    /*-------------------------------------*\
                    | Find the controller thread matching   |
                    | this ID                               |
                    \*-------------------------------------*/
                    std::size_t controller_thread_idx   = 0;
                    bool        match                   = false;

                    controller_threads_mutex.lock_shared();

                    for(; controller_thread_idx < controller_threads.size(); controller_thread_idx++)
                    {
                        if(client_info->client_protocol_version >= 6)
                        {
                            if(controller_threads[controller_thread_idx]->id == header.pkt_dev_id)
                            {
                                match = true;
                                break;
                            }
                        }
                        else
                        {
                            if(controller_threads[controller_thread_idx]->index == header.pkt_dev_id)
                            {
                                match = true;
                                break;
                            }
                        }
                    }

                    /*-------------------------------------*\
                    | If this ID exists in the list, queue  |
                    | the message to the matching thread    |
                    \*-------------------------------------*/
                    if(match)
                    {
                        controller_threads[controller_thread_idx]->queue_mutex.lock();

                        NetworkServerControllerThreadQueueEntry new_entry;
                        new_entry.data                      = data;
                        new_entry.header                    = header;
                        new_entry.client_info               = client_info;

                        controller_threads[controller_thread_idx]->queue.push(new_entry);
                        controller_threads[controller_thread_idx]->queue_mutex.unlock();
                        controller_threads[controller_thread_idx]->start_cv.notify_all();

                        delete_data = false;
                    }

                    controller_threads_mutex.unlock_shared();
                }
                break;

            case NET_PACKET_ID_RGBCONTROLLER_UPDATESINGLELED:
                status = ProcessRequest_RGBController_UpdateSingleLED(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_SETCUSTOMMODE:
                status = ProcessRequest_RGBController_SetCustomMode(client_info, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_CLEARSEGMENTS:
                status = ProcessRequest_RGBController_ClearSegments(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_ADDSEGMENT:
                status = ProcessRequest_RGBController_AddSegment(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_CONFIGUREZONE:
                status = ProcessRequest_RGBController_ConfigureZone(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_CONFIGUREDEVICE:
                status = ProcessRequest_RGBController_ConfigureDevice(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_SETHIDDEN:
                status = ProcessRequest_RGBController_SetHidden(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_SETDEVICESPECIFICCONFIGURATION:
                status = ProcessRequest_RGBController_SetDeviceSpecificConfiguration(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            case NET_PACKET_ID_RGBCONTROLLER_SETDEVICESPECIFICZONECONFIGURATION:
                status = ProcessRequest_RGBController_SetDeviceSpecificZoneConfiguration(client_info, header.pkt_size, data, header.pkt_dev_id);
                break;

            default:
                status = NET_PACKET_STATUS_ERROR_UNSUPPORTED;
                break;
        }

        if(delete_data)
        {
            delete[] data;

            SendAck(client_info, header.pkt_dev_id, header.pkt_id, status);
        }
    }

listen_done:

    ServerClientsMutex.lock();

    for(unsigned int this_idx = 0; this_idx < ServerClients.size(); this_idx++)
    {
        if(ServerClients[this_idx] == client_info)
        {
            delete client_info;
            ServerClients.erase(ServerClients.begin() + this_idx);
            break;
        }
    }

    client_info = nullptr;

    ServerClientsMutex.unlock();

    /*-----------------------------------------------------*\
    | Client info has changed, call the callbacks           |
    \*-----------------------------------------------------*/
    SignalClientInfoChanged();
}

/*---------------------------------------------------------*\
| Server Protocol functions                                 |
\*---------------------------------------------------------*/
NetPacketStatus NetworkServer::ProcessRequest_ClientFlags(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    if(data_size == sizeof(unsigned int))
    {
        ServerClientsMutex.lock();
        client_info->client_flags = *(unsigned int *)data_ptr;
        ServerClientsMutex.unlock();

        /*-------------------------------------------------*\
        | Client info has changed, call the callbacks       |
        \*-------------------------------------------------*/
        SignalClientInfoChanged();

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
}

NetPacketStatus NetworkServer::ProcessRequest_ClientProtocolVersion(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    unsigned int protocol_version = 0;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    if(data_size == sizeof(unsigned int))
    {
        memcpy(&protocol_version, data_ptr, sizeof(unsigned int));
    }

    if(protocol_version > OPENRGB_SDK_PROTOCOL_VERSION)
    {
        protocol_version = OPENRGB_SDK_PROTOCOL_VERSION;
    }

    ServerClientsMutex.lock();
    client_info->client_protocol_version = protocol_version;
    ServerClientsMutex.unlock();

    /*-----------------------------------------------------*\
    | Client info has changed, call the callbacks           |
    \*-----------------------------------------------------*/
    SignalClientInfoChanged();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_ClientString(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    ServerClientsMutex.lock();
    client_info->client_string.assign((char*)data_ptr, data_size);
    client_info->client_string = StringUtils::remove_null_terminating_chars(client_info->client_string);
    ServerClientsMutex.unlock();

    /*-------------------------------------------------*\
    | Client info has changed, call the callbacks       |
    \*-------------------------------------------------*/
    SignalClientInfoChanged();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RescanDevices()
{
    ResourceManager::get()->RescanDevices();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_GetHIDDeviceInfo(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    std::vector<HIDDeviceInfo>  device_info     = ResourceManager::get()->GetHIDDeviceInfo();

    unsigned int                data_size       = 0;
    unsigned short              device_count    = (unsigned short)device_info.size();

    /*-----------------------------------------------------*\
    | Calculate data size                                   |
    \*-----------------------------------------------------*/
    data_size                                  += sizeof(data_size);
    data_size                                  += sizeof(device_count);

    for(unsigned short device_idx = 0; device_idx < device_count; device_idx++)
    {
        data_size                              += sizeof(device_info[device_idx].vendor_id);
        data_size                              += sizeof(device_info[device_idx].product_id);
        data_size                              += sizeof(device_info[device_idx].release_number);
        data_size                              += sizeof(device_info[device_idx].usage_page);
        data_size                              += sizeof(device_info[device_idx].usage);
        data_size                              += sizeof(device_info[device_idx].interface_number);
        data_size                              += sizeof(unsigned short);
        data_size                              += (unsigned int)strlen(device_info[device_idx].serial_number.c_str()) + 1;
        data_size                              += sizeof(unsigned short);
        data_size                              += (unsigned int)strlen(device_info[device_idx].manufacturer_string.c_str()) + 1;
        data_size                              += sizeof(unsigned short);
        data_size                              += (unsigned int)strlen(device_info[device_idx].product_string.c_str()) + 1;
        data_size                              += sizeof(unsigned short);
        data_size                              += (unsigned int)strlen(device_info[device_idx].path.c_str()) + 1;
    }

    /*-----------------------------------------------------*\
    | Copy in data                                          |
    \*-----------------------------------------------------*/
    unsigned char*              data_buf        = new unsigned char[data_size];
    unsigned char*              data_ptr        = data_buf;

    memcpy(data_ptr, &data_size, sizeof(data_size));
    data_ptr += sizeof(data_size);

    memcpy(data_ptr, &device_count, sizeof(device_count));
    data_ptr += sizeof(device_count);

    for(unsigned int device_idx = 0; device_idx < device_count; device_idx++)
    {
        memcpy(data_ptr, &device_info[device_idx].vendor_id, sizeof(device_info[device_idx].vendor_id));
        data_ptr += sizeof(device_info[device_idx].vendor_id);

        memcpy(data_ptr, &device_info[device_idx].product_id, sizeof(device_info[device_idx].product_id));
        data_ptr += sizeof(device_info[device_idx].product_id);

        memcpy(data_ptr, &device_info[device_idx].release_number, sizeof(device_info[device_idx].release_number));
        data_ptr += sizeof(device_info[device_idx].release_number);

        memcpy(data_ptr, &device_info[device_idx].usage_page, sizeof(device_info[device_idx].usage_page));
        data_ptr += sizeof(device_info[device_idx].usage_page);

        memcpy(data_ptr, &device_info[device_idx].usage, sizeof(device_info[device_idx].usage));
        data_ptr += sizeof(device_info[device_idx].usage);

        memcpy(data_ptr, &device_info[device_idx].interface_number, sizeof(device_info[device_idx].interface_number));
        data_ptr += sizeof(device_info[device_idx].interface_number);

        unsigned short serial_number_size = (unsigned short)strlen(device_info[device_idx].serial_number.c_str()) + 1;
        memcpy(data_ptr, &serial_number_size, sizeof(serial_number_size));
        data_ptr += sizeof(serial_number_size);
        memcpy(data_ptr, device_info[device_idx].serial_number.c_str(), serial_number_size);
        data_ptr += serial_number_size;

        unsigned short manufacturer_string_size = (unsigned short)strlen(device_info[device_idx].manufacturer_string.c_str()) + 1;
        memcpy(data_ptr, &manufacturer_string_size, sizeof(manufacturer_string_size));
        data_ptr += sizeof(manufacturer_string_size);
        memcpy(data_ptr, device_info[device_idx].manufacturer_string.c_str(), manufacturer_string_size);
        data_ptr += manufacturer_string_size;

        unsigned short product_string_size = (unsigned short)strlen(device_info[device_idx].product_string.c_str()) + 1;
        memcpy(data_ptr, &product_string_size, sizeof(product_string_size));
        data_ptr += sizeof(product_string_size);
        memcpy(data_ptr, device_info[device_idx].product_string.c_str(), product_string_size);
        data_ptr += product_string_size;

        unsigned short path_size = (unsigned short)strlen(device_info[device_idx].path.c_str()) + 1;
        memcpy(data_ptr, &path_size, sizeof(path_size));
        data_ptr += sizeof(path_size);
        memcpy(data_ptr, device_info[device_idx].path.c_str(), path_size);
        data_ptr += path_size;
    }

    NetPacketHeader reply_hdr;

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_GET_HID_DEVICE_INFO, data_size);

    send_in_progress.lock();
    send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (char *)data_buf, reply_hdr.pkt_size, MSG_NOSIGNAL);
    send_in_progress.unlock();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_GetI2CBusInfo(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    std::vector<i2c_smbus_info> bus_info    = ResourceManager::get()->GetI2CBusInfo();

    unsigned int                data_size   = 0;
    unsigned short              bus_count   = (unsigned short)bus_info.size();
    data_size                              += sizeof(data_size);
    data_size                              += sizeof(bus_count);
    data_size                              += bus_count * sizeof(i2c_smbus_info);

    unsigned char*              data_buf    = new unsigned char[data_size];
    unsigned char*              data_ptr    = data_buf;

    memcpy(data_ptr, &data_size, sizeof(data_size));
    data_ptr += sizeof(data_size);

    memcpy(data_ptr, &bus_count, sizeof(bus_count));
    data_ptr += sizeof(bus_count);

    for(unsigned short bus_idx = 0; bus_idx < bus_count; bus_idx++)
    {
        memcpy(data_ptr, &bus_info[bus_idx], sizeof(bus_info[bus_idx]));
        data_ptr += sizeof(bus_info[bus_idx]);
    }

    NetPacketHeader reply_hdr;

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_GET_I2C_BUS_INFO, data_size);

    send_in_progress.lock();
    send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (char *)data_buf, reply_hdr.pkt_size, MSG_NOSIGNAL);
    send_in_progress.unlock();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_GetSerialPorts(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    std::vector<std::string>    serial_ports    = ResourceManager::get()->GetSerialPorts();

    unsigned int                data_size       = 0;
    unsigned short              port_count      = (unsigned short)serial_ports.size();

    /*-----------------------------------------------------*\
    | Calculate data size                                   |
    \*-----------------------------------------------------*/
    data_size                                  += sizeof(data_size);
    data_size                                  += sizeof(port_count);

    for(unsigned short port_idx = 0; port_idx < port_count; port_idx++)
    {
        data_size                              += sizeof(unsigned short);
        data_size                              += (unsigned int)strlen(serial_ports[port_idx].c_str()) + 1;
    }

    /*-----------------------------------------------------*\
    | Copy in data                                          |
    \*-----------------------------------------------------*/
    unsigned char*              data_buf        = new unsigned char[data_size];
    unsigned char*              data_ptr        = data_buf;

    memcpy(data_ptr, &data_size, sizeof(data_size));
    data_ptr += sizeof(data_size);

    memcpy(data_ptr, &port_count, sizeof(port_count));
    data_ptr += sizeof(port_count);

    for(unsigned short port_idx = 0; port_idx < port_count; port_idx++)
    {
        unsigned short port_string_size = (unsigned short)strlen(serial_ports[port_idx].c_str()) + 1;
        memcpy(data_ptr, &port_string_size, sizeof(port_string_size));
        data_ptr += sizeof(port_string_size);
        memcpy(data_ptr, serial_ports[port_idx].c_str(), port_string_size);
        data_ptr += port_string_size;
    }

    NetPacketHeader reply_hdr;

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_GET_SERIAL_PORTS, data_size);

    send_in_progress.lock();
    send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (char *)data_buf, reply_hdr.pkt_size, MSG_NOSIGNAL);
    send_in_progress.unlock();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_GetUSBDeviceInfo(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    std::vector<USBDeviceInfo>  device_info     = ResourceManager::get()->GetUSBDeviceInfo();

    unsigned int                data_size       = 0;
    unsigned short              device_count    = (unsigned short)device_info.size();

    /*-----------------------------------------------------*\
    | Calculate data size                                   |
    \*-----------------------------------------------------*/
    data_size                                  += sizeof(data_size);
    data_size                                  += sizeof(device_count);

    for(unsigned short device_idx = 0; device_idx < device_count; device_idx++)
    {
        data_size                              += sizeof(device_info[device_idx].vendor_id);
        data_size                              += sizeof(device_info[device_idx].product_id);
        data_size                              += sizeof(unsigned short);
        data_size                              += (unsigned int)strlen(device_info[device_idx].serial_number.c_str()) + 1;
        data_size                              += sizeof(unsigned short);
        data_size                              += (unsigned int)strlen(device_info[device_idx].manufacturer_string.c_str()) + 1;
        data_size                              += sizeof(unsigned short);
        data_size                              += (unsigned int)strlen(device_info[device_idx].product_string.c_str()) + 1;
    }

    /*-----------------------------------------------------*\
    | Copy in data                                          |
    \*-----------------------------------------------------*/
    unsigned char*              data_buf        = new unsigned char[data_size];
    unsigned char*              data_ptr        = data_buf;

    memcpy(data_ptr, &data_size, sizeof(data_size));
    data_ptr += sizeof(data_size);

    memcpy(data_ptr, &device_count, sizeof(device_count));
    data_ptr += sizeof(device_count);

    for(unsigned int device_idx = 0; device_idx < device_count; device_idx++)
    {
        memcpy(data_ptr, &device_info[device_idx].vendor_id, sizeof(device_info[device_idx].vendor_id));
        data_ptr += sizeof(device_info[device_idx].vendor_id);

        memcpy(data_ptr, &device_info[device_idx].product_id, sizeof(device_info[device_idx].product_id));
        data_ptr += sizeof(device_info[device_idx].product_id);

        unsigned short serial_number_size = (unsigned short)strlen(device_info[device_idx].serial_number.c_str()) + 1;
        memcpy(data_ptr, &serial_number_size, sizeof(serial_number_size));
        data_ptr += sizeof(serial_number_size);
        memcpy(data_ptr, device_info[device_idx].serial_number.c_str(), serial_number_size);
        data_ptr += serial_number_size;

        unsigned short manufacturer_string_size = (unsigned short)strlen(device_info[device_idx].manufacturer_string.c_str()) + 1;
        memcpy(data_ptr, &manufacturer_string_size, sizeof(manufacturer_string_size));
        data_ptr += sizeof(manufacturer_string_size);
        memcpy(data_ptr, device_info[device_idx].manufacturer_string.c_str(), manufacturer_string_size);
        data_ptr += manufacturer_string_size;

        unsigned short product_string_size = (unsigned short)strlen(device_info[device_idx].product_string.c_str()) + 1;
        memcpy(data_ptr, &product_string_size, sizeof(product_string_size));
        data_ptr += sizeof(product_string_size);
        memcpy(data_ptr, device_info[device_idx].product_string.c_str(), product_string_size);
        data_ptr += product_string_size;
    }

    NetPacketHeader reply_hdr;

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_GET_USB_DEVICE_INFO, data_size);

    send_in_progress.lock();
    send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (char *)data_buf, reply_hdr.pkt_size, MSG_NOSIGNAL);
    send_in_progress.unlock();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_GetUSBSerialPorts(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    std::vector<SerialDeviceInfo>   device_info     = ResourceManager::get()->GetUSBSerialPorts();

    unsigned int                    data_size       = 0;
    unsigned short                  device_count    = (unsigned short)device_info.size();

    /*-----------------------------------------------------*\
    | Calculate data size                                   |
    \*-----------------------------------------------------*/
    data_size                                      += sizeof(data_size);
    data_size                                      += sizeof(device_count);

    for(unsigned short device_idx = 0; device_idx < device_count; device_idx++)
    {
        data_size                                  += sizeof(device_info[device_idx].vendor_id);
        data_size                                  += sizeof(device_info[device_idx].product_id);
        data_size                                  += sizeof(unsigned short);
        data_size                                  += (unsigned int)strlen(device_info[device_idx].port_path.c_str()) + 1;
        data_size                                  += sizeof(unsigned short);
        data_size                                  += (unsigned int)strlen(device_info[device_idx].usb_path.c_str()) + 1;
    }

    /*-----------------------------------------------------*\
    | Copy in data                                          |
    \*-----------------------------------------------------*/
    unsigned char*                  data_buf        = new unsigned char[data_size];
    unsigned char*                  data_ptr        = data_buf;

    memcpy(data_ptr, &data_size, sizeof(data_size));
    data_ptr += sizeof(data_size);

    memcpy(data_ptr, &device_count, sizeof(device_count));
    data_ptr += sizeof(device_count);

    for(unsigned int device_idx = 0; device_idx < device_count; device_idx++)
    {
        memcpy(data_ptr, &device_info[device_idx].vendor_id, sizeof(device_info[device_idx].vendor_id));
        data_ptr += sizeof(device_info[device_idx].vendor_id);

        memcpy(data_ptr, &device_info[device_idx].product_id, sizeof(device_info[device_idx].product_id));
        data_ptr += sizeof(device_info[device_idx].product_id);

        unsigned short port_path_size = (unsigned short)strlen(device_info[device_idx].port_path.c_str()) + 1;
        memcpy(data_ptr, &port_path_size, sizeof(port_path_size));
        data_ptr += sizeof(port_path_size);
        memcpy(data_ptr, device_info[device_idx].port_path.c_str(), port_path_size);
        data_ptr += port_path_size;

        unsigned short usb_path_size = (unsigned short)strlen(device_info[device_idx].usb_path.c_str()) + 1;
        memcpy(data_ptr, &usb_path_size, sizeof(usb_path_size));
        data_ptr += sizeof(usb_path_size);
        memcpy(data_ptr, device_info[device_idx].usb_path.c_str(), usb_path_size);
        data_ptr += usb_path_size;
    }

    NetPacketHeader reply_hdr;

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_GET_USB_SERIAL_PORTS, data_size);

    send_in_progress.lock();
    send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (char *)data_buf, reply_hdr.pkt_size, MSG_NOSIGNAL);
    send_in_progress.unlock();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_LogManager_ClearLogBuffer(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    LogManager::get()->ClearLogBuffer();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_LogManager_GetLogBuffer(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    for(PLogMessage& logged_entry: LogManager::get()->GetLogBuffer())
    {
        /*-------------------------------------------------*\
        | Create data buffer for message                    |
        \*-------------------------------------------------*/
        unsigned int    data_size       = 0;
        unsigned short  filename_size   = (unsigned short)strlen(logged_entry->filename.c_str()) + 1;
        unsigned short  text_size       = (unsigned short)strlen(logged_entry->text.c_str()) + 1;

        data_size                      += sizeof(data_size);
        data_size                      += sizeof(logged_entry->level);
        data_size                      += sizeof(logged_entry->line);
        data_size                      += sizeof(filename_size);
        data_size                      += filename_size;
        data_size                      += sizeof(text_size);
        data_size                      += text_size;

        unsigned char*  data_buf        = new unsigned char[data_size];
        unsigned char*  data_ptr        = data_buf;

        memcpy(data_ptr, &data_size, sizeof(data_size));
        data_ptr += sizeof(data_size);

        memcpy(data_ptr, &logged_entry->level, sizeof(logged_entry->level));
        data_ptr += sizeof(logged_entry->level);

        memcpy(data_ptr, &logged_entry->line, sizeof(logged_entry->line));
        data_ptr += sizeof(logged_entry->line);

        memcpy(data_ptr, &filename_size, sizeof(filename_size));
        data_ptr += sizeof(filename_size);

        memcpy(data_ptr, logged_entry->filename.c_str(), filename_size);
        data_ptr += filename_size;

        memcpy(data_ptr, &text_size, sizeof(text_size));
        data_ptr += sizeof(text_size);

        memcpy(data_ptr, logged_entry->text.c_str(), text_size);
        data_ptr += text_size;

        /*---------------------------------------------*\
        | Send Logged Entry request to client           |
        \*---------------------------------------------*/
        SendRequest_LoggedEntry(client_info, data_size, data_buf);

        delete[] data_buf;
    }

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_LogManager_GetLogLevel(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    unsigned int log_level = LogManager::get()->GetLogLevel();

    NetPacketHeader reply_hdr;

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_LOGMANAGER_GET_LOG_LEVEL, sizeof(log_level));

    send_in_progress.lock();
    send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (char *)&log_level, reply_hdr.pkt_size, MSG_NOSIGNAL);
    send_in_progress.unlock();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_LogManager_SetLogLevel(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    unsigned char*  data_start  = data_ptr;
    unsigned int    log_level;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(data_size < sizeof(log_level))
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    COPY_DATA_FIELD(data_ptr, data_start, log_level);

    LogManager::get()->SetLogLevel(log_level);

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_ProfileManager_ClearActiveProfile(NetworkClientInfo* /*client_info*/)
{
    if(profile_manager)
    {
        profile_manager->ClearActiveProfile();

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_ProfileManager_DeleteProfile(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    if(profile_manager)
    {
        std::string profile_name;
        profile_name.assign((char*)data_ptr, data_size);
        profile_name = StringUtils::remove_null_terminating_chars(profile_name);

        profile_manager->DeleteProfile(profile_name);

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_ProfileManager_DownloadProfile(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    if(profile_manager)
    {
        std::string profile_name;
        profile_name.assign((char*)data_ptr, data_size);
        profile_name = StringUtils::remove_null_terminating_chars(profile_name);

        std::string profile_json_string = profile_manager->ReadProfileJSON(profile_name).dump();

        NetPacketHeader reply_hdr;

        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_PROFILEMANAGER_DOWNLOAD_PROFILE, (unsigned int)strlen(profile_json_string.c_str()) + 1);

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (char *)profile_json_string.c_str(), reply_hdr.pkt_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_ProfileManager_GetActiveProfile(NetworkClientInfo* client_info)
{
    if(profile_manager)
    {
        std::string active_profile_name = profile_manager->GetActiveProfile();

        NetPacketHeader reply_hdr;

        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_PROFILEMANAGER_GET_ACTIVE_PROFILE, (unsigned int)strlen(active_profile_name.c_str()) + 1);

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (char *)active_profile_name.c_str(), reply_hdr.pkt_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_ProfileManager_GetProfileList(NetworkClientInfo* client_info)
{
    if(profile_manager)
    {
        NetPacketHeader reply_hdr;
        unsigned char*  reply_data = profile_manager->GetProfileListDescription();
        unsigned int    reply_size;

        memcpy(&reply_size, reply_data, sizeof(reply_size));

        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_PROFILEMANAGER_GET_PROFILE_LIST, reply_size);

        send_in_progress.lock();
        send(client_info->client_sock, (const char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (const char *)reply_data, reply_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        delete[] reply_data;

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_ProfileManager_LoadProfile(NetworkClientInfo* /*client_info*/, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    if(profile_manager)
    {
        std::string profile_name;
        profile_name.assign((char*)data_ptr, data_size);
        profile_name = StringUtils::remove_null_terminating_chars(profile_name);

        if(profile_manager->LoadProfile(profile_name))
        {
            return(NET_PACKET_STATUS_OK);
        }
        else
        {
            return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
        }
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_ProfileManager_SaveProfile(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    if(profile_manager)
    {
        std::string profile_name;
        profile_name.assign((char*)data_ptr, data_size);
        profile_name = StringUtils::remove_null_terminating_chars(profile_name);

        if(profile_manager->SaveProfile(profile_name))
        {
            return(NET_PACKET_STATUS_OK);
        }
        else
        {
            return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
        }
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_ProfileManager_UploadProfile(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    if(profile_manager)
    {
        std::string profile_json_string;
        profile_json_string.assign((char*)data_ptr, data_size);
        profile_json_string = StringUtils::remove_null_terminating_chars(profile_json_string);

        nlohmann::json profile_json;
        JsonUtils::JsonParse(profile_json_string, profile_json);

        if(profile_manager->SaveProfileFromJSON(profile_json))
        {
            return(NET_PACKET_STATUS_OK);
        }
        else
        {
            return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
        }
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_SettingsManager_GetSettings(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    if(settings_manager != NULL)
    {
        std::string settings_key;
        settings_key.assign((char*)data_ptr, data_size);
        settings_key = StringUtils::remove_null_terminating_chars(settings_key);

        nlohmann::json settings_json = settings_manager->GetSettings(settings_key);
        std::string settings_json_str = settings_json.dump();

        NetPacketHeader reply_hdr;

        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_SETTINGSMANAGER_GET_SETTINGS, (unsigned int)strlen(settings_json_str.c_str()) + 1);

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (char *)settings_json_str.c_str(), reply_hdr.pkt_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_SettingsManager_GetSettingsSchema(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    if(settings_manager != NULL)
    {
        std::string settings_key;
        settings_key.assign((char*)data_ptr, data_size);
        settings_key = StringUtils::remove_null_terminating_chars(settings_key);

        nlohmann::json settings_json = settings_manager->GetSettingsSchema(settings_key);
        std::string settings_json_str = settings_json.dump();

        NetPacketHeader reply_hdr;

        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_SETTINGSMANAGER_GET_SETTINGS_SCHEMA, (unsigned int)strlen(settings_json_str.c_str()) + 1);

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (char *)settings_json_str.c_str(), reply_hdr.pkt_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_SettingsManager_ModifySettings(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    if(settings_manager != NULL)
    {
        std::string settings_json_str;
        settings_json_str.assign((char*)data_ptr, data_size);
        settings_json_str = StringUtils::remove_null_terminating_chars(settings_json_str);

        settings_manager->ModifySettingsFromJsonString(settings_json_str);

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_SettingsManager_SetSettings(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    if(settings_manager != NULL)
    {
        std::string settings_json_str;
        settings_json_str.assign((char*)data_ptr, data_size);
        settings_json_str = StringUtils::remove_null_terminating_chars(settings_json_str);

        settings_manager->SetSettingsFromJsonString(settings_json_str);

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_SettingsManager_SaveSettings(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Require local client for this packet                  |
    \*-----------------------------------------------------*/
    if(!client_info->client_is_local_client)
    {
        return(NET_PACKET_STATUS_ERROR_NOT_ALLOWED);
    }

    if(settings_manager != NULL)
    {
        settings_manager->SaveSettings();

        return(NET_PACKET_STATUS_OK);
    }

    return(NET_PACKET_STATUS_ERROR_UNSUPPORTED);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_AddSegment(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned int    data_size_pkt;
    unsigned char*  data_start              = data_ptr;
    segment         new_segment;
    int             zone_idx;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in data size                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, data_size_pkt);

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(data_size_pkt != data_size)
    {
        LOG_ERROR("[%s] AddSegment packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Copy in zone index                                    |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, zone_idx);

    /*-----------------------------------------------------*\
    | Add new segment                                       |
    \*-----------------------------------------------------*/
    data_ptr = controllers[controller_idx]->SetSegmentDescription(data_ptr, data_size - (unsigned int)(data_ptr - data_start), &new_segment, client_info->client_protocol_version);

    /*-----------------------------------------------------*\
    | If data pointer is null after parsing segment         |
    | description, return                                   |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    controllers[controller_idx]->AddSegment(zone_idx, new_segment);

    /*-----------------------------------------------------*\
    | Save configuration                                    |
    \*-----------------------------------------------------*/
    profile_manager->SaveConfiguration();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_ClearSegments(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned char*  data_start              = data_ptr;
    int             zone_idx;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(data_size != sizeof(zone_idx))
    {
        LOG_ERROR("[%s] ClearSegments packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in zone index                                    |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, zone_idx);

    /*-----------------------------------------------------*\
    | Call ClearSegments on the given controller            |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->ClearSegments(zone_idx);

    /*-----------------------------------------------------*\
    | Save configuration                                    |
    \*-----------------------------------------------------*/
    profile_manager->SaveConfiguration();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_ConfigureZone(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned int    data_size_pkt;
    unsigned char*  data_start              = data_ptr;
    int             zone_idx;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in data size                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, data_size_pkt);

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(data_size_pkt != data_size)
    {
        LOG_ERROR("[%s] ConfigureZone packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Copy in zone index                                    |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, zone_idx);

    /*-----------------------------------------------------*\
    | Configure zone                                        |
    \*-----------------------------------------------------*/
    zone new_zone;

    data_ptr = controllers[controller_idx]->SetZoneDescription(data_ptr, data_size - (unsigned int)(data_ptr - data_start), &new_zone, client_info->client_protocol_version);

    /*-----------------------------------------------------*\
    | If data pointer is null after parsing zone            |
    | description, return                                   |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    controllers[controller_idx]->ConfigureZone(zone_idx, new_zone);

    /*-----------------------------------------------------*\
    | Save configuration                                    |
    \*-----------------------------------------------------*/
    profile_manager->SaveConfiguration();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_ResizeZone(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned char*  data_start              = data_ptr;
    int             new_size;
    int             zone_idx;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(data_size != (sizeof(zone_idx) + sizeof(new_size)))
    {
        LOG_ERROR("[%s] ResizeZone packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in zone index                                    |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, zone_idx);

    /*-----------------------------------------------------*\
    | Copy in new zone size                                 |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, new_size);

    /*-----------------------------------------------------*\
    | Call ResizeZone on the given controller               |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->ResizeZone(zone_idx, new_size);

    /*-----------------------------------------------------*\
    | Save configuration                                    |
    \*-----------------------------------------------------*/
    profile_manager->SaveConfiguration();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_ConfigureDevice(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool                controller_idx_valid;
    unsigned int        controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned int        data_size_pkt;
    unsigned char*      data_start              = data_ptr;
    controller_flags    new_flags;
    unsigned short      new_name_size;
    std::string         new_name;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in data size                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, data_size_pkt);

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(data_size_pkt != data_size)
    {
        LOG_ERROR("[%s] ConfigureDevice packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Copy in flags                                         |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, new_flags);

    /*-----------------------------------------------------*\
    | Copy in name                                          |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, new_name_size);
    COPY_STRING_FIELD(data_ptr, data_start, new_name_size, new_name);

    controllers[controller_idx]->ConfigureDevice(new_flags, new_name);

    /*-----------------------------------------------------*\
    | Save configuration                                    |
    \*-----------------------------------------------------*/
    profile_manager->SaveConfiguration();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_SetCustomMode(NetworkClientInfo* client_info, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Call SetCustomMode on the given controller            |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->SetCustomMode();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_SetDeviceSpecificConfiguration(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    std::string     configuration_string;
    nlohmann::json  configuration;
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned char*  data_start              = data_ptr;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Save configuration JSON string                        |
    \*-----------------------------------------------------*/
    COPY_STRING_FIELD(data_ptr, data_start, data_size, configuration_string);

    JsonUtils::JsonParse(configuration_string, configuration);

    controllers[controller_idx]->SetDeviceSpecificConfiguration(configuration);

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_SetDeviceSpecificZoneConfiguration(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    unsigned int    configuration_string_size;
    std::string     configuration_string;
    nlohmann::json  configuration;
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned char*  data_start              = data_ptr;
    int             zone_idx;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Save configuration JSON string                        |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, zone_idx);
    COPY_DATA_FIELD(data_ptr, data_start, configuration_string_size);
    COPY_STRING_FIELD(data_ptr, data_start, configuration_string_size, configuration_string);

    JsonUtils::JsonParse(configuration_string, configuration);

    controllers[controller_idx]->SetDeviceSpecificZoneConfiguration(zone_idx, configuration);

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_SetHidden(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If data size is invalid, return                       |
    \*-----------------------------------------------------*/
    if(data_size < sizeof(bool))
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    controllers[controller_idx]->SetHidden(*(bool*)data_ptr);

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_UpdateLEDs(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned int    data_size_pkt;
    unsigned char*  data_start              = data_ptr;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in data size                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, data_size_pkt);

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(!(legacy_workaround_enabled && (client_info->client_protocol_version <= 4)) && (data_size_pkt != data_size))
    {
        LOG_ERROR("[%s] UpdateLEDs packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Lock access mutex                                     |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.lock();

    /*-----------------------------------------------------*\
    | Update colors                                         |
    \*-----------------------------------------------------*/
    data_ptr = RGBController::SetColorDescription(data_ptr, data_size - (unsigned int)(data_ptr - data_start), controllers[controller_idx], client_info->client_protocol_version);

    /*-----------------------------------------------------*\
    | Unlock access mutex                                   |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.unlock();

    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Call UpdateLEDs on the given controller               |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->UpdateLEDs();

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_UpdateSaveMode(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id, bool save_mode)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned int    data_size_pkt;
    unsigned char*  data_start              = data_ptr;
    int             mode_idx;
    mode            temp_mode;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in data size                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, data_size_pkt);

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(!(legacy_workaround_enabled && (client_info->client_protocol_version <= 4)) && (data_size_pkt != data_size))
    {
        LOG_ERROR("[%s] UpdateSaveMode packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Copy in mode index                                    |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, mode_idx);

    /*-----------------------------------------------------*\
    | Lock access mutex                                     |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.lock();

    /*-----------------------------------------------------*\
    | Check if we aren't reading beyond the list of modes.  |
    \*-----------------------------------------------------*/
    if(((std::size_t)mode_idx) >= controllers[controller_idx]->modes.size())
    {
        /*-------------------------------------------------*\
        | Unlock access mutex                               |
        \*-------------------------------------------------*/
        controllers[controller_idx]->AccessMutex.unlock();
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Read mode description                                 |
    \*-----------------------------------------------------*/
    data_ptr = RGBController::SetModeDescription(data_ptr, data_size - (unsigned int)(data_ptr - data_start), &temp_mode, client_info->client_protocol_version);

    /*-----------------------------------------------------*\
    | Set mode values and active mode                       |
    \*-----------------------------------------------------*/
    if((mode_idx >= 0) && ((std::size_t)mode_idx < controllers[controller_idx]->modes.size()))
    {
        if(RGBController::SetModeValuesFromMode(controllers[controller_idx]->modes[mode_idx], temp_mode))
        {
            controllers[controller_idx]->active_mode = mode_idx;
        }
    }

    /*-----------------------------------------------------*\
    | Unlock access mutex                                   |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.unlock();

    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Call either SaveMode or UpdateMode on the given       |
    | controller                                            |
    \*-----------------------------------------------------*/
    if(save_mode)
    {
        controllers[controller_idx]->SaveMode();
    }
    else
    {
        controllers[controller_idx]->UpdateMode();
    }

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_UpdateSingleLED(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned char*  data_start              = data_ptr;
    int             led_idx;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(data_size != (sizeof(led_idx) + sizeof(RGBColor)))
    {
        LOG_ERROR("[%s] UpdateSingleLED packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Lock access mutex                                     |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.lock();

    /*-----------------------------------------------------*\
    | Fixed size descrption:                                |
    |       int:      LED index                             |
    |       RGBColor: LED color                             |
    |                                                       |
    | Copy in LED index                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD_UNLOCK(data_ptr, data_start, led_idx, controllers[controller_idx]);

    /*-----------------------------------------------------*\
    | Check if we aren't reading beyond the list of leds.   |
    \*-----------------------------------------------------*/
    if(((size_t)led_idx) >= controllers[controller_idx]->leds.size())
    {
        /*-------------------------------------------------*\
        | Unlock access mutex                               |
        \*-------------------------------------------------*/
        controllers[controller_idx]->AccessMutex.unlock();
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Copy in LED color                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD_UNLOCK(data_ptr, data_start, controllers[controller_idx]->colors[led_idx], controllers[controller_idx]);

    /*-----------------------------------------------------*\
    | Unlock access mutex                                   |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.unlock();

    /*-----------------------------------------------------*\
    | Call UpdateSingleLED                                  |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->UpdateSingleLED(led_idx);

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_UpdateZoneLEDs(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned int    data_size_pkt;
    unsigned char*  data_start              = data_ptr;
    unsigned short  num_colors;
    unsigned int    zone_idx;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in data size                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, data_size_pkt);

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(!(legacy_workaround_enabled && (client_info->client_protocol_version <= 4)) && (data_size_pkt != data_size))
    {
        LOG_ERROR("[%s] UpdateZoneLEDs packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Lock access mutex                                     |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.lock();

    /*-----------------------------------------------------*\
    | Copy in zone index                                    |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD_UNLOCK(data_ptr, data_start, zone_idx, controllers[controller_idx]);

    /*-----------------------------------------------------*\
    | Check if we aren't reading beyond the list of zones.  |
    \*-----------------------------------------------------*/
    if(((std::size_t)zone_idx) >= controllers[controller_idx]->zones.size())
    {
        /*-------------------------------------------------*\
        | Unlock access mutex                               |
        \*-------------------------------------------------*/
        controllers[controller_idx]->AccessMutex.unlock();
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Copy in number of colors                              |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD_UNLOCK(data_ptr, data_start, num_colors, controllers[controller_idx]);

    /*-----------------------------------------------------*\
    | Copy in colors                                        |
    \*-----------------------------------------------------*/
    if(((std::size_t)num_colors) > controllers[controller_idx]->zones[zone_idx].leds_count)
    {
        /*-------------------------------------------------*\
        | Unlock access mutex                               |
        \*-------------------------------------------------*/
        controllers[controller_idx]->AccessMutex.unlock();
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    for(unsigned short color_idx = 0; color_idx < num_colors; color_idx++)
    {
        COPY_DATA_FIELD_UNLOCK(data_ptr, data_start, controllers[controller_idx]->zones[zone_idx].colors[color_idx], controllers[controller_idx]);
    }

    /*-----------------------------------------------------*\
    | Unlock access mutex                                   |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.unlock();

    /*-----------------------------------------------------*\
    | Call UpdateZoneLEDs                                   |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->UpdateZoneLEDs(zone_idx);

    return(NET_PACKET_STATUS_OK);
}

NetPacketStatus NetworkServer::ProcessRequest_RGBController_UpdateZoneMode(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int controller_id)
{
    /*-----------------------------------------------------*\
    | Convert ID to index                                   |
    \*-----------------------------------------------------*/
    bool            controller_idx_valid;
    unsigned int    controller_idx          = index_from_id(controller_id, client_info->client_protocol_version, &controller_idx_valid);
    unsigned int    data_size_pkt;
    unsigned char*  data_start              = data_ptr;
    int             mode_idx;
    mode            temp_mode;
    int             zone_idx;

    /*-----------------------------------------------------*\
    | If data pointer is null, return                       |
    \*-----------------------------------------------------*/
    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | If controller ID is invalid, return                   |
    \*-----------------------------------------------------*/
    if(!controller_idx_valid)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_ID);
    }

    /*-----------------------------------------------------*\
    | Copy in data size                                     |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD(data_ptr, data_start, data_size_pkt);

    /*-----------------------------------------------------*\
    | If packet size is invalid, return                     |
    \*-----------------------------------------------------*/
    if(data_size_pkt != data_size)
    {
        LOG_ERROR("[%s] UpdateZoneMode packet has invalid size. Packet size: %d", data_size, NETWORKSERVER);

        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Lock access mutex                                     |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.lock();

    /*-----------------------------------------------------*\
    | Copy in zone index                                    |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD_UNLOCK(data_ptr, data_start, zone_idx, controllers[controller_idx]);

    /*-----------------------------------------------------*\
    | Copy in mode index                                    |
    \*-----------------------------------------------------*/
    COPY_DATA_FIELD_UNLOCK(data_ptr, data_start, mode_idx, controllers[controller_idx]);

    /*-----------------------------------------------------*\
    | Check if we aren't reading beyond the list of modes.  |
    \*-----------------------------------------------------*/
    if((((std::size_t)zone_idx) >= controllers[controller_idx]->zones.size()) || (mode_idx > (int)controllers[controller_idx]->zones[zone_idx].modes.size()))
    {
        /*-------------------------------------------------*\
        | Unlock access mutex                               |
        \*-------------------------------------------------*/
        controllers[controller_idx]->AccessMutex.unlock_shared();
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Read mode description                                 |
    \*-----------------------------------------------------*/
    if(mode_idx >= 0)
    {
        data_ptr = RGBController::SetModeDescription(data_ptr, data_size - (unsigned int)(data_ptr - data_start), &temp_mode, client_info->client_protocol_version);
    }

    /*-----------------------------------------------------*\
    | Set mode values and active mode                       |
    \*-----------------------------------------------------*/
    if((mode_idx >= 0) && ((std::size_t)mode_idx < controllers[controller_idx]->zones[zone_idx].modes.size()))
    {
        if(RGBController::SetModeValuesFromMode(controllers[controller_idx]->zones[zone_idx].modes[mode_idx], temp_mode))
        {
            controllers[controller_idx]->zones[zone_idx].active_mode = mode_idx;
        }
    }

    /*-----------------------------------------------------*\
    | Unlock access mutex                                   |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->AccessMutex.unlock();

    if(data_ptr == NULL)
    {
        return(NET_PACKET_STATUS_ERROR_INVALID_DATA);
    }

    /*-----------------------------------------------------*\
    | Update zone mode                                      |
    \*-----------------------------------------------------*/
    controllers[controller_idx]->UpdateZoneMode(zone_idx);

    return(NET_PACKET_STATUS_OK);
}

void NetworkServer::SendAck(NetworkClientInfo* client_info, unsigned int acked_pkt_dev_id, unsigned int acked_pkt_id, NetPacketStatus status)
{
    /*-----------------------------------------------------*\
    | ACKs were introduced in protocol version 6            |
    \*-----------------------------------------------------*/
    if(client_info->client_protocol_version < 6)
    {
        return;
    }

    NetPacketHeader ack_hdr;
    NetPacketAck    ack_data;

    InitNetPacketHeader(&ack_hdr, acked_pkt_dev_id, NET_PACKET_ID_ACK, sizeof(NetPacketAck));

    ack_data.acked_pkt_id   = acked_pkt_id;
    ack_data.status         = status;

    send_in_progress.lock();
    send(client_info->client_sock, (const char *)&ack_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (const char *)&ack_data, sizeof(ack_data), MSG_NOSIGNAL);
    send_in_progress.unlock();
}

void NetworkServer::SendReply_ControllerCount(NetworkClientInfo* client_info)
{
    controller_ids_mutex.lock_shared();

    NetPacketHeader reply_hdr;
    unsigned int    controller_count    = (unsigned int)controller_ids.size();
    unsigned int    data_size           = 0;

    /*-----------------------------------------------------*\
    | Determine data size                                   |
    \*-----------------------------------------------------*/
    data_size                          += sizeof(controller_count);

    /*-----------------------------------------------------*\
    | Starting with protocol > 6, a list of controller IDs  |
    | is sent in addition to the size                       |
    \*-----------------------------------------------------*/
    if(client_info->client_protocol_version >= 6)
    {
        data_size                      += (controller_count * sizeof(unsigned int));
    }

    unsigned char*  data_buf            = new unsigned char[data_size];
    unsigned char*  data_ptr            = data_buf;

    memcpy(data_ptr, &controller_count, sizeof(controller_count));
    data_ptr += sizeof(controller_count);

    if(client_info->client_protocol_version >= 6)
    {
        for(unsigned int controller_id_idx = 0; controller_id_idx < controller_count; controller_id_idx++)
        {
            memcpy(data_ptr, &controller_ids[controller_id_idx].id, sizeof(controller_ids[controller_id_idx].id));
            data_ptr += sizeof(controller_ids[controller_id_idx].id);
        }
    }

    controller_ids_mutex.unlock_shared();

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_REQUEST_CONTROLLER_COUNT, data_size);

    controller_count = (unsigned int)controllers.size();

    send_in_progress.lock();
    send(client_info->client_sock, (const char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (const char *)data_buf, data_size, MSG_NOSIGNAL);
    send_in_progress.unlock();

    delete[] data_buf;
}

void NetworkServer::SendReply_ControllerData(NetworkClientInfo* client_info, unsigned int dev_id, unsigned int protocol_version)
{
    /*---------------------------------------------------------*\
    | Convert ID to index                                       |
    \*---------------------------------------------------------*/
    bool            idx_valid;
    unsigned int    dev_idx = index_from_id(dev_id, protocol_version, &idx_valid);

    if(idx_valid && (dev_idx < controller_ids.size()))
    {
        NetPacketHeader reply_hdr;
        unsigned char*  reply_data;
        unsigned int    reply_size;

        /*-----------------------------------------------------*\
        | Lock controller's access mutex                        |
        \*-----------------------------------------------------*/
        controllers[dev_idx]->AccessMutex.lock_shared();

        /*-----------------------------------------------------*\
        | Get data size                                         |
        \*-----------------------------------------------------*/
        reply_size                  = sizeof(reply_size);
        reply_size                 += RGBController::GetDeviceDescriptionSize(controllers[dev_idx], protocol_version);

        /*-----------------------------------------------------*\
        | Create data buffer                                    |
        \*-----------------------------------------------------*/
        reply_data                  = new unsigned char[reply_size];
        unsigned char*  data_ptr    = reply_data;

        /*-----------------------------------------------------*\
        | Copy in data size                                     |
        \*-----------------------------------------------------*/
        memcpy(data_ptr, &reply_size, sizeof(reply_size));
        data_ptr += sizeof(reply_size);

        /*-----------------------------------------------------*\
        | Copy in data                                          |
        \*-----------------------------------------------------*/
        data_ptr                    = RGBController::GetDeviceDescriptionData(data_ptr, controllers[dev_idx], protocol_version);

        /*-----------------------------------------------------*\
        | Unlock controller's access mutex                      |
        \*-----------------------------------------------------*/
        controllers[dev_idx]->AccessMutex.unlock_shared();

        /*-----------------------------------------------------*\
        | Initialize packet header                              |
        \*-----------------------------------------------------*/
        InitNetPacketHeader(&reply_hdr, dev_id, NET_PACKET_ID_REQUEST_CONTROLLER_DATA, reply_size);

        /*-----------------------------------------------------*\
        | Send packet                                           |
        \*-----------------------------------------------------*/
        send_in_progress.lock();
        send(client_info->client_sock, (const char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (const char *)reply_data, reply_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        /*-----------------------------------------------------*\
        | Delete data                                           |
        \*-----------------------------------------------------*/
        delete[] reply_data;
    }
}

void NetworkServer::SendReply_ProtocolVersion(NetworkClientInfo* client_info)
{
    NetPacketHeader reply_hdr;
    unsigned int    reply_data;

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_REQUEST_PROTOCOL_VERSION, sizeof(unsigned int));

    reply_data = OPENRGB_SDK_PROTOCOL_VERSION;

    send_in_progress.lock();
    send(client_info->client_sock, (const char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (const char *)&reply_data, sizeof(unsigned int), MSG_NOSIGNAL);
    send_in_progress.unlock();
}

void NetworkServer::SendReply_ServerFlags(NetworkClientInfo* client_info)
{
    /*-----------------------------------------------------*\
    | Send server flags to client only if protocol is 6 or  |
    | greater                                               |
    \*-----------------------------------------------------*/
    ServerClientsMutex.lock();
    if(client_info->client_protocol_version >= 6)
    {
        unsigned int    flags_value = server_flags;
        NetPacketHeader reply_hdr;

        /*-------------------------------------------------*\
        | If client requested local client, grant it if the |
        | client is a local connection                      |
        \*-------------------------------------------------*/
        if((client_info->client_flags & NET_CLIENT_FLAG_REQUEST_LOCAL_CLIENT)
        && (client_info->client_is_local))
        {
            client_info->client_is_local_client = true;
            flags_value |= NET_SERVER_FLAG_LOCAL_CLIENT;
        }

        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_SET_SERVER_FLAGS, sizeof(flags_value));

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (char *)&flags_value, reply_hdr.pkt_size, MSG_NOSIGNAL);
        send_in_progress.unlock();
    }
    ServerClientsMutex.unlock();
}

void NetworkServer::SendReply_ServerString(NetworkClientInfo* client_info)
{
    /*---------------------------------------------------------*\
    | Send server string to client only if protocol is 6 or     |
    | greater                                                   |
    \*---------------------------------------------------------*/
    ServerClientsMutex.lock();
    if(client_info->client_protocol_version >= 6)
    {
        NetPacketHeader reply_hdr;

        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_SET_SERVER_NAME, (unsigned int)strlen(server_name.c_str()) + 1);

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (char *)server_name.c_str(), reply_hdr.pkt_size, MSG_NOSIGNAL);
        send_in_progress.unlock();
    }
    ServerClientsMutex.unlock();
}

void NetworkServer::SendReply_PluginList(NetworkClientInfo* client_info)
{
    unsigned int data_size = 0;
    unsigned int data_ptr = 0;

    /*---------------------------------------------------------*\
    | Calculate data size                                       |
    \*---------------------------------------------------------*/
    unsigned short num_plugins = (unsigned short)plugin_manager->GetPluginCount();

    data_size += sizeof(data_size);
    data_size += sizeof(num_plugins);

    for(unsigned int i = 0; i < num_plugins; i++)
    {
        data_size += sizeof(unsigned short) * 3;
        data_size += (unsigned int)strlen(plugin_manager->GetPluginName(i).c_str()) + 1;
        data_size += (unsigned int)strlen(plugin_manager->GetPluginDescription(i).c_str()) + 1;
        data_size += (unsigned int)strlen(plugin_manager->GetPluginVersion(i).c_str()) + 1;
        data_size += sizeof(unsigned int) * 2;
    }

    /*---------------------------------------------------------*\
    | Create data buffer                                        |
    \*---------------------------------------------------------*/
    unsigned char* data_buf = new unsigned char[data_size];

    /*---------------------------------------------------------*\
    | Copy in data size                                         |
    \*---------------------------------------------------------*/
    memcpy(&data_buf[data_ptr], &data_size, sizeof(data_size));
    data_ptr += sizeof(data_size);

    /*---------------------------------------------------------*\
    | Copy in num_plugins                                       |
    \*---------------------------------------------------------*/
    memcpy(&data_buf[data_ptr], &num_plugins, sizeof(num_plugins));
    data_ptr += sizeof(num_plugins);

    for(unsigned int i = 0; i < num_plugins; i++)
    {
        /*---------------------------------------------------------*\
        | Copy in plugin name (size+data)                           |
        \*---------------------------------------------------------*/
        unsigned short str_len = (unsigned short)strlen(plugin_manager->GetPluginName(i).c_str()) + 1;

        memcpy(&data_buf[data_ptr], &str_len, sizeof(unsigned short));
        data_ptr += sizeof(unsigned short);

        strcpy((char *)&data_buf[data_ptr], plugin_manager->GetPluginName(i).c_str());
        data_ptr += str_len;

        /*---------------------------------------------------------*\
        | Copy in plugin description (size+data)                    |
        \*---------------------------------------------------------*/
        str_len = (unsigned short)strlen(plugin_manager->GetPluginDescription(i).c_str()) + 1;

        memcpy(&data_buf[data_ptr], &str_len, sizeof(unsigned short));
        data_ptr += sizeof(unsigned short);

        strcpy((char *)&data_buf[data_ptr], plugin_manager->GetPluginDescription(i).c_str());
        data_ptr += str_len;

        /*---------------------------------------------------------*\
        | Copy in plugin version (size+data)                        |
        \*---------------------------------------------------------*/
        str_len = (unsigned short)strlen(plugin_manager->GetPluginVersion(i).c_str()) + 1;

        memcpy(&data_buf[data_ptr], &str_len, sizeof(unsigned short));
        data_ptr += sizeof(unsigned short);

        strcpy((char *)&data_buf[data_ptr], plugin_manager->GetPluginVersion(i).c_str());
        data_ptr += str_len;

        /*---------------------------------------------------------*\
        | Copy in plugin index (data)                               |
        \*---------------------------------------------------------*/
        memcpy(&data_buf[data_ptr], &i, sizeof(unsigned int));
        data_ptr += sizeof(unsigned int);

        /*---------------------------------------------------------*\
        | Copy in plugin sdk version (data)                         |
        \*---------------------------------------------------------*/
        unsigned int protocol_version = plugin_manager->GetPluginProtocolVersion(i);
        memcpy(&data_buf[data_ptr], &protocol_version, sizeof(unsigned int));
        data_ptr += sizeof(unsigned int);
    }

    NetPacketHeader reply_hdr;
    unsigned int reply_size;

    memcpy(&reply_size, data_buf, sizeof(reply_size));

    InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_PLUGINMANAGER_GET_PLUGIN_LIST, reply_size);

    send_in_progress.lock();
    send(client_info->client_sock, (const char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (const char *)data_buf, reply_size, MSG_NOSIGNAL);
    send_in_progress.unlock();

    delete [] data_buf;
}

void NetworkServer::SendReply_PluginSpecific(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr, unsigned int pkt_id)
{
    NetPacketHeader reply_hdr;

    if(data_ptr != NULL)
    {
        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_PLUGINMANAGER_PLUGIN_SPECIFIC, data_size + sizeof(pkt_id));

        send_in_progress.lock();
        send(client_info->client_sock, (const char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (const char *)&pkt_id, sizeof(pkt_id), MSG_NOSIGNAL);
        send(client_info->client_sock, (const char *)data_ptr, data_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        delete [] data_ptr;
    }
}

void NetworkServer::SendRequest_LoggedEntry(NetworkClientInfo* client_info, unsigned int data_size, unsigned char* data_ptr)
{
    NetPacketHeader pkt_hdr;

    if(data_ptr != NULL)
    {
        InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_LOGMANAGER_LOGGED_ENTRY, data_size);

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (char *)data_ptr, pkt_hdr.pkt_size, MSG_NOSIGNAL);
        send_in_progress.unlock();
    }
}

void NetworkServer::SendRequest_ProfileManager_ActiveProfileChanged(NetworkClientInfo* client_info, std::string active_profile)
{
    NetPacketHeader pkt_hdr;

    InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_PROFILEMANAGER_ACTIVE_PROFILE_CHANGED, (unsigned int)strlen(active_profile.c_str()) + 1);

    send_in_progress.lock();
    send(client_info->client_sock, (char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (char *)active_profile.c_str(), pkt_hdr.pkt_size, MSG_NOSIGNAL);
    send_in_progress.unlock();
}

void NetworkServer::SendRequest_DetectionCompleted(NetworkClientInfo* client_info)
{
    if(client_info->client_protocol_version >= 6)
    {
        NetPacketHeader pkt_hdr;

        InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_DETECTION_COMPLETE, 0);

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send_in_progress.unlock();
    }
}

void NetworkServer::SendRequest_DetectionProgress(NetworkClientInfo* client_info, unsigned int detection_percent, std::string detection_string)
{
    if(client_info->client_protocol_version >= 6)
    {
        unsigned int    data_size;
        unsigned short  string_length;
        NetPacketHeader reply_hdr;

        string_length               = (unsigned short)strlen(detection_string.c_str()) + 1;
        data_size                   = sizeof(data_size);
        data_size                  += sizeof(detection_percent);
        data_size                  += sizeof(string_length);
        data_size                  += string_length;

        unsigned char*  data_buf    = new unsigned char[data_size];
        unsigned char*  data_ptr    = data_buf;

        memcpy(data_ptr, &data_size, sizeof(data_size));
        data_ptr += sizeof(data_size);

        memcpy(data_ptr, &detection_percent, sizeof(detection_percent));
        data_ptr += sizeof(detection_percent);

        memcpy(data_ptr, &string_length, sizeof(string_length));
        data_ptr += sizeof(string_length);

        memcpy(data_ptr, detection_string.c_str(), string_length);
        data_ptr += string_length;

        InitNetPacketHeader(&reply_hdr, 0, NET_PACKET_ID_DETECTION_PROGRESS_CHANGED, data_size);

        send_in_progress.lock();
        send(client_info->client_sock, (const char *)&reply_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (const char *)data_buf, data_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        delete [] data_buf;
    }
}

void NetworkServer::SendRequest_DetectionStarted(NetworkClientInfo* client_info)
{
    if(client_info->client_protocol_version >= 6)
    {
        NetPacketHeader pkt_hdr;

        InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_DETECTION_STARTED, 0);

        send_in_progress.lock();
        send(client_info->client_sock, (char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send_in_progress.unlock();
    }
}

void NetworkServer::SendRequest_DeviceListChanged(NetworkClientInfo* client_info)
{
    NetPacketHeader pkt_hdr;

    InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_DEVICE_LIST_UPDATED, 0);

    send_in_progress.lock();
    send(client_info->client_sock, (char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send_in_progress.unlock();
}

void NetworkServer::ProfileManager_ProfileAboutToLoad()
{
    SendRequest_ProfileManager_ProfileAboutToLoad();

    /*-----------------------------------------------------*\
    | Wait for about to load completion or timeout          |
    \*-----------------------------------------------------*/
    unsigned int timeout = 0;

    while(true)
    {
        /*-------------------------------------------------*\
        | Test for the following conditions:                |
        |   1.  All acks to ProfileAboutToLoad have been    |
        |       received from clients                       |
        |   2.  All RGBController queues have been emptied  |
        |   3.  We haven't exceeded the 1 second timeout    |
        \*-------------------------------------------------*/
        bool all_acks_received = (profile_about_to_load_acks >= profile_about_to_load_count);

        bool all_rgbcontroller_queues_emptied = true;

        controller_threads_mutex.lock_shared();
        for(std::size_t controller_thread_idx = 0; controller_thread_idx < controller_threads.size(); controller_thread_idx++)
        {
            if(controller_threads[controller_thread_idx]->queue.size() > 0)
            {
                all_rgbcontroller_queues_emptied = false;
                break;
            }
        }
        controller_threads_mutex.unlock_shared();

        bool timed_out = (timeout >= 1000);

        if((all_acks_received && all_rgbcontroller_queues_emptied) || timed_out)
        {
            break;
        }

        timeout++;
        std::this_thread::sleep_for(1ms);
    }
}

void NetworkServer::SendRequest_ProfileManager_ActiveProfileChanged(std::string profile_name)
{
    NetPacketHeader pkt_hdr;

    InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_PROFILEMANAGER_ACTIVE_PROFILE_CHANGED, (unsigned int)strlen(profile_name.c_str()) + 1);

    for(std::size_t client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        if(ServerClients[client_idx]->client_flags & NET_CLIENT_FLAG_SUPPORTS_PROFILEMANAGER)
        {
            SOCKET client_sock = ServerClients[client_idx]->client_sock;

            send_in_progress.lock();
            send(client_sock, (char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
            send(client_sock, (char *)profile_name.c_str(), pkt_hdr.pkt_size, MSG_NOSIGNAL);
            send_in_progress.unlock();
        }
    }
}

void NetworkServer::SendRequest_ProfileManager_ProfileAboutToLoad()
{
    NetPacketHeader pkt_hdr;

    InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_PROFILEMANAGER_PROFILE_ABOUT_TO_LOAD, 0);

    profile_about_to_load_acks = 0;
    profile_about_to_load_count = 0;

    for(std::size_t client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        if(ServerClients[client_idx]->client_flags & NET_CLIENT_FLAG_SUPPORTS_PROFILEMANAGER)
        {
            profile_about_to_load_count++;

            SOCKET client_sock = ServerClients[client_idx]->client_sock;

            send_in_progress.lock();
            send(client_sock, (char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
            send_in_progress.unlock();
        }
    }
}

void NetworkServer::SendRequest_ProfileManager_ProfileListChanged(NetworkClientInfo* client_info, unsigned char* profile_list_description)
{
    NetPacketHeader pkt_hdr;
    unsigned int    pkt_size;

    memcpy(&pkt_size, profile_list_description, sizeof(pkt_size));

    InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_PROFILEMANAGER_PROFILE_LIST_UPDATED, pkt_size);

    send_in_progress.lock();
    send(client_info->client_sock, (const char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
    send(client_info->client_sock, (const char *)profile_list_description, pkt_size, MSG_NOSIGNAL);
    send_in_progress.unlock();
}

void NetworkServer::SendRequest_ProfileManager_ProfileLoaded(std::string profile_json_string)
{
    NetPacketHeader pkt_hdr;

    InitNetPacketHeader(&pkt_hdr, 0, NET_PACKET_ID_PROFILEMANAGER_PROFILE_LOADED, (unsigned int)strlen(profile_json_string.c_str()) + 1);

    for(std::size_t client_idx = 0; client_idx < ServerClients.size(); client_idx++)
    {
        if((ServerClients[client_idx]->client_flags & NET_CLIENT_FLAG_SUPPORTS_PROFILEMANAGER) && (ServerClients[client_idx]->client_is_local_client))
        {
            SOCKET client_sock = ServerClients[client_idx]->client_sock;

            send_in_progress.lock();
            send(client_sock, (char *)&pkt_hdr, sizeof(NetPacketHeader), MSG_NOSIGNAL);
            send(client_sock, (char *)profile_json_string.c_str(), pkt_hdr.pkt_size, MSG_NOSIGNAL);
            send_in_progress.unlock();
        }
    }
}

void NetworkServer::SendRequest_RGBController_SignalUpdate(RGBController * controller_ptr, unsigned int update_reason)
{
    /*-----------------------------------------------------*\
    | Find the index of this controller in the list         |
    \*-----------------------------------------------------*/
    std::size_t                 controller_idx      = 0;
    bool                        found               = false;
    unsigned int                controller_id;

    for(; controller_idx < controllers.size(); controller_idx++)
    {
        if(controller_ids[controller_idx].controller == controller_ptr)
        {
            controller_id   = controller_ids[controller_idx].id;
            found           = true;
            break;
        }
    }

    /*-----------------------------------------------------*\
    | Only send SignalUpdate requests if the controller is  |
    | in the list                                           |
    \*-----------------------------------------------------*/
    if(found)
    {
        /*-------------------------------------------------*\
        | Queue to each connected client with protocol 6+.  |
        | ServerClientsMutex is held across the loop so a   |
        | client cannot be deleted while we enqueue to it.  |
        \*-------------------------------------------------*/
        ServerClientsMutex.lock();

        for(std::size_t client_idx = 0; client_idx < ServerClients.size(); client_idx++)
        {
            NetworkClientInfo* client_info = ServerClients[client_idx];

            if(client_info->client_protocol_version >= 6)
            {
                unsigned int    protocol_version    = client_info->client_protocol_version;
                NetPacketHeader reply_hdr;
                unsigned char*  reply_data;
                unsigned int    reply_size;

                /*-----------------------------------------*\
                | Lock controller's access mutex            |
                \*-----------------------------------------*/
                controller_ptr->AccessMutex.lock_shared();

                /*-----------------------------------------*\
                | Get data size                             |
                \*-----------------------------------------*/
                reply_size                          = sizeof(reply_size);
                reply_size                         += sizeof(update_reason);

                switch(update_reason)
                {
                    /*-------------------------------------*\
                    | UpdateLEDs() sends color description  |
                    \*-------------------------------------*/
                    case RGBCONTROLLER_UPDATE_REASON_UPDATELEDS:
                        reply_size                 += RGBController::GetColorDescriptionSize(controller_ptr, protocol_version);
                        break;

                    /*-------------------------------------*\
                    | Everything else send controller       |
                    | description                           |
                    \*-------------------------------------*/
                    case RGBCONTROLLER_UPDATE_REASON_UPDATEMODE:
                    case RGBCONTROLLER_UPDATE_REASON_SAVEMODE:
                    case RGBCONTROLLER_UPDATE_REASON_CONFIGUREZONE:
                    case RGBCONTROLLER_UPDATE_REASON_CLEARSEGMENTS:
                    case RGBCONTROLLER_UPDATE_REASON_ADDSEGMENT:
                    case RGBCONTROLLER_UPDATE_REASON_HIDDEN:
                    case RGBCONTROLLER_UPDATE_REASON_UNHIDDEN:
                    default:
                        reply_size                 += RGBController::GetDeviceDescriptionSize(controller_ptr, protocol_version);
                        break;
                }

                /*-----------------------------------------*\
                | Create data buffer                        |
                \*-----------------------------------------*/
                reply_data                          = new unsigned char[reply_size];
                unsigned char*  data_ptr            = reply_data;

                /*-----------------------------------------*\
                | Copy in data size                         |
                \*-----------------------------------------*/
                memcpy(data_ptr, &reply_size, sizeof(reply_size));
                data_ptr += sizeof(reply_size);

                /*-----------------------------------------*\
                | Copy in update reason                     |
                \*-----------------------------------------*/
                memcpy(data_ptr, &update_reason, sizeof(update_reason));
                data_ptr += sizeof(update_reason);

                /*-----------------------------------------*\
                | Copy in data                              |
                \*-----------------------------------------*/
                switch(update_reason)
                {
                    /*-------------------------------------*\
                    | UpdateLEDs() sends color description  |
                    \*-------------------------------------*/
                    case RGBCONTROLLER_UPDATE_REASON_UPDATELEDS:
                        data_ptr                    = RGBController::GetColorDescriptionData(data_ptr, controller_ptr, protocol_version);
                        break;

                    /*-------------------------------------*\
                    | Everything else send controller       |
                    | description                           |
                    \*-------------------------------------*/
                    case RGBCONTROLLER_UPDATE_REASON_UPDATEMODE:
                    case RGBCONTROLLER_UPDATE_REASON_SAVEMODE:
                    case RGBCONTROLLER_UPDATE_REASON_CONFIGUREZONE:
                    case RGBCONTROLLER_UPDATE_REASON_CLEARSEGMENTS:
                    case RGBCONTROLLER_UPDATE_REASON_ADDSEGMENT:
                    case RGBCONTROLLER_UPDATE_REASON_HIDDEN:
                    case RGBCONTROLLER_UPDATE_REASON_UNHIDDEN:
                    case RGBCONTROLLER_UPDATE_REASON_SETDEVICESPECIFICCONFIGURATION:
                    case RGBCONTROLLER_UPDATE_REASON_SETDEVICESPECIFICZONECONFIGURATION:
                    default:
                        data_ptr                    = RGBController::GetDeviceDescriptionData(data_ptr, controller_ptr, protocol_version);
                        break;
                }

                /*-----------------------------------------*\
                | Unlock controller's access mutex          |
                \*-----------------------------------------*/
                controller_ptr->AccessMutex.unlock_shared();

                /*-----------------------------------------*\
                | Initialize packet header                  |
                | Since this packet is only supported on    |
                | protocols 6+, we do not need to handle    |
                | index based controller IDs.               |
                \*-----------------------------------------*/
                InitNetPacketHeader(&reply_hdr, controller_id, NET_PACKET_ID_RGBCONTROLLER_SIGNALUPDATE, reply_size);

                /*-----------------------------------------*\
                | Queue the packet for the send thread.     |
                | This runs inside SignalUpdate, so a       |
                | blocking send would stall the callback    |
                | drain and deadlock a rescan. The entry    |
                | takes ownership of reply_data; the send   |
                | thread frees it after the send, so it is  |
                | safe to send after the controller is      |
                | gone.                                     |
                \*-----------------------------------------*/
                {
                    std::lock_guard<std::mutex> queue_lock(client_info->client_send_mutex);

                    if(update_reason == RGBCONTROLLER_UPDATE_REASON_UPDATELEDS)
                    {
                        /*---------------------------------*\
                        | Coalesce: a newer frame for       |
                        | this controller replaces the      |
                        | pending one, keeping the queue    |
                        | bounded under a stalled client.   |
                        \*---------------------------------*/
                        std::map<unsigned int, std::list<NetworkServerClientSendQueueEntry>::iterator>::iterator existing = client_info->client_send_coalesce.find(controller_id);

                        if(existing != client_info->client_send_coalesce.end())
                        {
                            delete[] existing->second->data;

                            existing->second->header = reply_hdr;
                            existing->second->data   = reply_data;
                        }
                        else
                        {
                            client_info->client_send_queue.push_back(NetworkServerClientSendQueueEntry{controller_id, true, reply_hdr, reply_data});
                            client_info->client_send_coalesce[controller_id] = std::prev(client_info->client_send_queue.end());
                        }
                    }
                    else
                    {
                        /*---------------------------------*\
                        | Non-coalescable event. Backstop   |
                        | a stalled client by dropping      |
                        | the oldest at the cap.            |
                        \*---------------------------------*/
                        if(client_info->client_send_queue.size() >= NETWORKSERVER_SIGNAL_QUEUE_MAX)
                        {
                            NetworkServerClientSendQueueEntry& oldest = client_info->client_send_queue.front();

                            if(oldest.coalescable)
                            {
                                client_info->client_send_coalesce.erase(oldest.controller_id);
                            }

                            delete[] oldest.data;

                            client_info->client_send_queue.pop_front();
                            LOG_WARNING("[%s] SignalUpdate queue full for client %s, dropping oldest packet", NETWORKSERVER, client_info->client_ip.c_str());
                        }

                        client_info->client_send_queue.push_back(NetworkServerClientSendQueueEntry{controller_id, false, reply_hdr, reply_data});
                    }
                }

                client_info->client_send_cv.notify_one();
            }
        }

        ServerClientsMutex.unlock();
    }
}

/*---------------------------------------------------------*\
| Per-client SignalUpdate send thread. The blocking send    |
| happens here, not inside SignalUpdate, so a slow client   |
| blocks only its own queue and thread. The callback drain  |
| is never blocked, so a rescan cannot deadlock on a send.  |
\*---------------------------------------------------------*/
void NetworkServer::ClientSendThreadFunction(NetworkClientInfo* client_info)
{
    while(true)
    {
        NetworkServerClientSendQueueEntry entry;

        {
            std::unique_lock<std::mutex> queue_lock(client_info->client_send_mutex);
            client_info->client_send_cv.wait(queue_lock, [client_info]{ return !client_info->client_send_queue.empty() || !client_info->client_send_running; });

            if(!client_info->client_send_running)
            {
                break;
            }

            entry = client_info->client_send_queue.front();

            if(entry.coalescable)
            {
                client_info->client_send_coalesce.erase(entry.controller_id);
            }

            client_info->client_send_queue.pop_front();
        }

        /*-------------------------------------------------*\
        | send_in_progress still serializes against         |
        | response sends to this client. Routing those      |
        | through a per-client mutex is the follow-up.      |
        \*-------------------------------------------------*/
        send_in_progress.lock();
        send(client_info->client_sock, (const char *)&entry.header, sizeof(NetPacketHeader), MSG_NOSIGNAL);
        send(client_info->client_sock, (const char *)entry.data, entry.header.pkt_size, MSG_NOSIGNAL);
        send_in_progress.unlock();

        /*-------------------------------------------------*\
        | The entry owns the data pointer; free it now that |
        | the send is done.                                 |
        \*-------------------------------------------------*/
        delete[] entry.data;
    }
}

/*---------------------------------------------------------*\
| Private helper functions                                  |
\*---------------------------------------------------------*/
int NetworkServer::accept_select(int sockfd)
{
    fd_set              set;
    struct timeval      timeout;

    while(1)
    {
        timeout.tv_sec          = TCP_TIMEOUT_SECONDS;
        timeout.tv_usec         = 0;

        FD_ZERO(&set);
        FD_SET(sockfd, &set);

        int rv = select(sockfd + 1, &set, NULL, NULL, &timeout);

        if(rv == SOCKET_ERROR || server_online == false)
        {
            return -1;
        }
        else if(rv == 0)
        {
            continue;
        }
        else
        {
            return(accept((int)sockfd, NULL, NULL));
        }
    }
}

unsigned int NetworkServer::index_from_id(unsigned int id, unsigned int protocol_version, bool* index_valid)
{
    /*-----------------------------------------------------*\
    | For protocol < 6, the ID is the index                 |
    \*-----------------------------------------------------*/
    unsigned int index  = id;
    *index_valid        = true;

    /*-----------------------------------------------------*\
    | For protocol >= 6, look up the ID from the index      |
    \*-----------------------------------------------------*/
    if(protocol_version >= 6)
    {
        controller_ids_mutex.lock_shared();

        *index_valid = false;

        for(std::size_t controller_id_idx = 0; controller_id_idx < controller_ids.size(); controller_id_idx++)
        {
            if(controller_ids[controller_id_idx].id == id)
            {
                index           = (unsigned int)controller_id_idx;
                *index_valid    = true;
                break;
            }
        }

        controller_ids_mutex.unlock_shared();
    }
    else
    {
        if(index >= controllers.size())
        {
            *index_valid = false;
        }
    }

    return(index);
}

int NetworkServer::recv_select(SOCKET s, char *buf, int len, int flags)
{
    fd_set              set;
    struct timeval      timeout;

    while(1)
    {
        timeout.tv_sec          = TCP_TIMEOUT_SECONDS;
        timeout.tv_usec         = 0;

        FD_ZERO(&set);
        FD_SET(s, &set);

        int rv = select((int)s + 1, &set, NULL, NULL, &timeout);

        if(rv == SOCKET_ERROR || server_online == false)
        {
            return 0;
        }
        else if(rv == 0)
        {
            continue;
        }
        else
        {
        /*-------------------------------------------------*\
        | Set QUICKACK socket option on Linux to improve    |
        | performance                                       |
        \*-------------------------------------------------*/
#ifdef __linux__
            setsockopt(s, IPPROTO_TCP, TCP_QUICKACK, &yes, sizeof(yes));
#endif
            return(recv(s, buf, len, flags));
        }
    }
}
