/*---------------------------------------------------------*\
| MSIMonitorController.cpp                                  |
|                                                           |
|   Driver for MSI monitor (gaming controller)              |
|                                                           |
|   Andy Herbert                              2026 June  4  |
|   Ken Sanislo                               2026 July 20  |
|                                                           |
|   This file is part of the OpenRGB project                |
|   SPDX-License-Identifier: GPL-2.0-or-later               |
\*---------------------------------------------------------*/

#include <chrono>
#include <string.h>
#include <thread>
#include "MSIMonitorController.h"
#include "StringUtils.h"

/*---------------------------------------------------------*\
| Feature reports are control transfers and this panel      |
| holds one outstanding for tens of milliseconds.  Back     |
| to back writes pin the shared control pipe at full        |
| duty and starve other non periodic traffic: a device      |
| with no interrupt OUT endpoint writes by SET_REPORT       |
| and stops answering, while its isochronous endpoints      |
| keep running.                                             |
|                                                           |
| Idle a fixed interval after each write to leave the       |
| control pipe free.                                        |
\*---------------------------------------------------------*/
void MSIMonitorController::SendFeatureReport(uint8_t *data, size_t length)
{
    hid_send_feature_report(dev, data, length);

    std::this_thread::sleep_for(std::chrono::milliseconds(MSI_MONITOR_PIPE_IDLE_MS));
}

MSIMonitorController::MSIMonitorController(hid_device* dev_handle, const hid_device_info& info, std::string dev_name)
{
    dev                 = dev_handle;
    location            = info.path;
    name                = dev_name;
}

MSIMonitorController::~MSIMonitorController()
{
    hid_close(dev);
}

std::string MSIMonitorController::GetDeviceLocation()
{
    return("HID: " + location);
}

std::string MSIMonitorController::GetNameString()
{
    return(name);
}

std::string MSIMonitorController::GetSerialString()
{
    wchar_t serial_string[128];
    int ret = hid_get_serial_number_string(dev, serial_string, 128);

    if(ret != 0)
    {
        return("");
    }

    return(StringUtils::wstring_to_string(serial_string));
}

void MSIMonitorController::Set(uint8_t mode_value, const std::vector<RGBColor> colors, uint8_t last_bit)
{
    /*---------------------------------------------------------*\
    | Prepare color data                                        |
    \*---------------------------------------------------------*/
    uint8_t data[MSI_MONITOR_PACKET_SIZE];
    memset(data, 0x00, MSI_MONITOR_PACKET_SIZE);

    unsigned int offset = 0;

    data[offset++] = 0x71;
    data[offset++] = 0x01;
    for(int i = 0; i < 3; i++)
    {
        data[offset++] = 0x00;
    }

    data[offset++] = 0x01;
    data[offset++] = 0x64;
    for(int i = 0; i < 5; i++)
    {
        data[offset++] = 0x00;
    }

    /*---------------------------------------------------------*\
    | put mode_value                                            |
    \*---------------------------------------------------------*/
    data[offset++] = mode_value;

    for(int i = 0; i < 3; i++)
    {
        data[offset++] = 0x00;
    }
    data[offset++] = 0x01;
    data[offset++] = 0x64;
    for(int i = 0; i < 5; i++)
    {
        data[offset++] = 0x00;
    }

    /*-----------------------------------------------------------------------*\
    | this data looks like placeholder for additional LEDs in other monitors  |
    \*-----------------------------------------------------------------------*/
    for(int i = 0; i < 9; i++)
    {
        data[offset++] = 0xff;
        data[offset++] = 0x00;
        data[offset++] = 0x00;
    }

    //RGB values begin
    for(const RGBColor color: colors)
    {
        data[offset++] = RGBGetRValue(color);
        data[offset++] = RGBGetGValue(color);
        data[offset++] = RGBGetBValue(color);
    }

    /*-------------------------------------------------------------------------------*\
    | last bit is probably a write to device bit- 0x01 saves to device, 0x00 doesn't. |
    | For direct mode, bit is set to 0x00, otherwise lights will flicker              |
    \*-------------------------------------------------------------------------------*/

    data[offset++] = last_bit;

    /*---------------------------------------------------------*\
    | Send the data (1 packet)                                  |
    \*---------------------------------------------------------*/

    SendFeatureReport(data, MSI_MONITOR_PACKET_SIZE);
}

uint8_t MSIMonitorController::GetLayoutVersion()
{
    /*-----------------------------------------------------*\
    | Ask the MCU which buffer layout it speaks.  Request   |
    | is [0x01][opcode] zero padded, reply is [0x01][0x5A]  |
    | then data with the version at byte 3.                 |
    \*-----------------------------------------------------*/
    uint8_t request[MSI_MONITOR_QUERY_PACKET_SIZE];
    uint8_t reply[MSI_MONITOR_QUERY_PACKET_SIZE];

    /*-----------------------------------------------------*\
    | Unimplemented opcodes never answer, so a reply left   |
    | queued by an earlier exchange would be read here and  |
    | parsed as this query's answer.  Drain first.          |
    \*-----------------------------------------------------*/
    while(hid_read_timeout(dev, reply, MSI_MONITOR_QUERY_PACKET_SIZE, 0) > 0)
    {
        continue;
    }

    memset(request, 0x00, MSI_MONITOR_QUERY_PACKET_SIZE);

    request[0] = MSI_MONITOR_QUERY_REPORT_ID;
    request[1] = MSI_MONITOR_OPCODE_GET_VERSION;

    if(hid_write(dev, request, MSI_MONITOR_QUERY_PACKET_SIZE) < 0)
    {
        return(MSI_MONITOR_VERSION_UNKNOWN);
    }

    memset(reply, 0x00, MSI_MONITOR_QUERY_PACKET_SIZE);

    if(hid_read_timeout(dev, reply, MSI_MONITOR_QUERY_PACKET_SIZE, 500) <= 0)
    {
        return(MSI_MONITOR_VERSION_UNKNOWN);
    }

    if(reply[1] != MSI_MONITOR_QUERY_ACK)
    {
        return(MSI_MONITOR_VERSION_UNKNOWN);
    }

    return(reply[3]);
}

void MSIMonitorController::SetMode72(uint8_t mode_value, uint8_t speed, uint8_t brightness, RGBColor color1, RGBColor color2, bool user_palette, bool save, const std::vector<RGBColor>& led_colors, bool fill_both_arrays)
{
    uint8_t data[MSI_MONITOR_72_PACKET_SIZE];
    memset(data, 0x00, MSI_MONITOR_72_PACKET_SIZE);

    data[0] = 0x72;

    /*-----------------------------------------------------*\
    | Identical 11 byte control blocks at index 1 and 12    |
    \*-----------------------------------------------------*/
    for(unsigned int base = 1; base <= 12; base += 11)
    {
        data[base + 0]  = mode_value;
        data[base + 1]  = RGBGetRValue(color1);
        data[base + 2]  = RGBGetGValue(color1);
        data[base + 3]  = RGBGetBValue(color1);
        data[base + 4]  = speed;
        data[base + 5]  = brightness;
        data[base + 6]  = RGBGetRValue(color2);
        data[base + 7]  = RGBGetGValue(color2);
        data[base + 8]  = RGBGetBValue(color2);
        data[base + 9]  = user_palette ? 0x80 : 0x00;
        data[base + 10] = 0x00;
    }

    /*-----------------------------------------------------*\
    | The engine paints from the second inline colour array |
    | at index 95, not from Colour1 and not from the first  |
    | array at index 23: on a 322URX a buffer carrying only |
    | Colour1 renders black, and walking one lit slot       |
    | through the buffer only lights LEDs for the second    |
    | array.  That pairs with the control block at index 12 |
    | being the operative one.  Built in palette modes      |
    | leave the array clear so the firmware effect is not   |
    | painted over.                                         |
    |                                                       |
    | fill_both_arrays also writes the first array at index |
    | 23, for the dual block versions (11/13/15) whose two  |
    | control blocks each drive their own run.  On a single |
    | block layout the first array is inert, so filling it  |
    | is harmless.  Those versions also route colour index  |
    | above 15 to a separate DragonShield emblem region not |
    | handled here - one reason they stay disabled.         |
    \*-----------------------------------------------------*/
    if(user_palette)
    {
        for(unsigned int slot = 0; slot < MSI_MONITOR_72_ARRAY_SLOTS && slot < led_colors.size(); slot++)
        {
            unsigned int offset_b = MSI_MONITOR_72_ARRAY_B_INDEX + (slot * 3);

            data[offset_b + 0] = RGBGetRValue(led_colors[slot]);
            data[offset_b + 1] = RGBGetGValue(led_colors[slot]);
            data[offset_b + 2] = RGBGetBValue(led_colors[slot]);

            if(fill_both_arrays)
            {
                unsigned int offset_a = MSI_MONITOR_72_ARRAY_A_INDEX + (slot * 3);

                data[offset_a + 0] = RGBGetRValue(led_colors[slot]);
                data[offset_a + 1] = RGBGetGValue(led_colors[slot]);
                data[offset_a + 2] = RGBGetBValue(led_colors[slot]);
            }
        }
    }

    /*-----------------------------------------------------*\
    | Final byte is store to flash: 0x01 persists across    |
    | power cycles, 0x00 applies without a flash write.     |
    | Keep 0x00 for streaming to avoid flicker and wear.    |
    \*-----------------------------------------------------*/
    data[MSI_MONITOR_72_STORE_INDEX] = save ? 0x01 : 0x00;

    SendFeatureReport(data, MSI_MONITOR_72_PACKET_SIZE);
}
