/*-----------------------------------------*\
|  ASRockPolychromeSMBusController.cpp      |
|                                           |
|  Driver for for ASRock ASR LED and        |
|  Polychrome RGB lighting controller       |
|                                           |
|  Adam Honse (CalcProgrammer1) 12/14/2019  |
\*-----------------------------------------*/

#include "ASRockPolychromeSMBusController.h"
#include <cstring>
#include "dependencies/dmiinfo.h"
#include "LogManager.h"

#define ASROCK_ZONE_LED_COUNT_MESSAGE_EN    "[%s] Zone %i LED count: %02d"

using namespace std::chrono_literals;

PolychromeController::PolychromeController(i2c_smbus_interface* bus, polychrome_dev_id dev)
{
    this->bus = bus;
    this->dev = dev;

    DMIInfo dmi;

    device_name =   "ASRock " + dmi.getMainboard();
    fw_version =    ReadFirmwareVersion();
    unsigned char   major_version = fw_version >> 8;

    /*-----------------------------------------------------*\
    | Determine whether the device uses ASR LED or          |
    | Polychrome protocol by checking firmware version.     |
    | Versions: 1.xx are ASR RGB LED                        |
    |           2.xx are Polychrome v1                      |
    |           3.xx are Polychrome v2                      |
    \*-----------------------------------------------------*/
    switch(major_version)
    {
        case ASROCK_TYPE_ASRLED:
            LOG_TRACE("[%s] Device type is ASR RGB LED", ASROCK_CONTROLLER_NAME);
            asrock_type = ASROCK_TYPE_ASRLED;
            memset(zone_led_count, 0, sizeof(zone_led_count));
            break;

        case ASROCK_TYPE_POLYCHROME_V1:
            LOG_TRACE("[%s] Device type is Polychrome v1", ASROCK_CONTROLLER_NAME);
            asrock_type = ASROCK_TYPE_POLYCHROME_V1;
            ReadLEDConfiguration();
            break;

        case ASROCK_TYPE_POLYCHROME_V2:
            LOG_TRACE("[%s] Device type is Polychrome v2", ASROCK_CONTROLLER_NAME);
            asrock_type = ASROCK_TYPE_POLYCHROME_V2;
            ReadLEDConfiguration();
            break;

        default:
            LOG_TRACE("[%s] Got Unknown version!", ASROCK_CONTROLLER_NAME);
            asrock_type = ASROCK_TYPE_UNKNOWN;
            break;
    }
}

PolychromeController::~PolychromeController()
{

}

unsigned int PolychromeController::GetASRockType()
{
    return(asrock_type);
}

std::string PolychromeController::GetDeviceLocation()
{
    std::string return_string(bus->device_name);
    char addr[5];
    snprintf(addr, 5, "0x%02X", dev);
    return_string.append(", address ");
    return_string.append(addr);
    return("I2C: " + return_string);
}

std::string PolychromeController::GetDeviceName()
{
    return(device_name);
}

std::string PolychromeController::GetFirmwareVersion()
{
    unsigned char  major_version = fw_version >> 8;
    unsigned char  minor_version = fw_version & 0xFF;

    return(std::to_string(major_version) + "." + std::to_string(minor_version));
}

unsigned short PolychromeController::ReadFirmwareVersion()
{
    // The firmware register holds two bytes, so the first read should return 2
    // If not, report invalid firmware revision FFFF
    LOG_DEBUG("[%s] Reading back device firmware version", ASROCK_CONTROLLER_NAME);
    // Version response array needs to be 32 bytes to prevent non ASRock board from stack smashing
    unsigned char asrock_version[I2C_SMBUS_BLOCK_MAX] = { 0x00, 0x00 };
    if (bus->i2c_smbus_read_block_data(dev, ASROCK_REG_FIRMWARE_VER, asrock_version) == 0x02)
    {
        unsigned char major = asrock_version[0];
        unsigned char minor = asrock_version[1];

        LOG_DEBUG("[%s] Device firmware version: v%02d.%02d", ASROCK_CONTROLLER_NAME, major, minor);
        return((major << 8) | minor);
    }
    else
    {
        LOG_WARNING("[%s] Firmware readback failed; Returning 0xFFFF", ASROCK_CONTROLLER_NAME);
        return(0xFFFF);
    }
}

void PolychromeController::ReadLEDConfiguration()
{
    /*---------------------------------------------------------------------------------*\
    | The LED configuration register holds 6 bytes, so the first read should return 6   |
    | If not, set all zone sizes to zero                                                |
    \*---------------------------------------------------------------------------------*/
    LOG_DEBUG("[%s] Reading LED config from controller", ASROCK_CONTROLLER_NAME);
    unsigned char asrock_zone_count[I2C_SMBUS_BLOCK_MAX] = { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 };
    if (bus->i2c_smbus_read_block_data(dev, POLYCHROME_REG_LED_CONFIG, asrock_zone_count) == 0x06)
    {
        zone_led_count[POLYCHROME_ZONE_1]           = asrock_zone_count[0];
        zone_led_count[POLYCHROME_ZONE_2]           = asrock_zone_count[1];
        zone_led_count[POLYCHROME_ZONE_3]           = asrock_zone_count[2];
        zone_led_count[POLYCHROME_ZONE_4]           = asrock_zone_count[3];
        zone_led_count[POLYCHROME_ZONE_5]           = asrock_zone_count[4];
        zone_led_count[POLYCHROME_ZONE_ADDRESSABLE] = asrock_zone_count[5];
        LOG_DEBUG(ASROCK_ZONE_LED_COUNT_MESSAGE_EN, ASROCK_CONTROLLER_NAME, POLYCHROME_ZONE_1, zone_led_count[POLYCHROME_ZONE_1]);
        LOG_DEBUG(ASROCK_ZONE_LED_COUNT_MESSAGE_EN, ASROCK_CONTROLLER_NAME, POLYCHROME_ZONE_2, zone_led_count[POLYCHROME_ZONE_2]);
        LOG_DEBUG(ASROCK_ZONE_LED_COUNT_MESSAGE_EN, ASROCK_CONTROLLER_NAME, POLYCHROME_ZONE_3, zone_led_count[POLYCHROME_ZONE_3]);
        LOG_DEBUG(ASROCK_ZONE_LED_COUNT_MESSAGE_EN, ASROCK_CONTROLLER_NAME, POLYCHROME_ZONE_4, zone_led_count[POLYCHROME_ZONE_4]);
        LOG_DEBUG(ASROCK_ZONE_LED_COUNT_MESSAGE_EN, ASROCK_CONTROLLER_NAME, POLYCHROME_ZONE_5, zone_led_count[POLYCHROME_ZONE_5]);
        LOG_DEBUG("[%s] Addressable Zone LED count: %02d", ASROCK_CONTROLLER_NAME, zone_led_count[POLYCHROME_ZONE_ADDRESSABLE]);
    }
    else
    {
        LOG_WARNING("[%s] LED config read failed", ASROCK_CONTROLLER_NAME);
        memset(zone_led_count, 0, sizeof(zone_led_count));
    }
}

unsigned int PolychromeController::GetMode()
{
    return(active_mode);
}

void PolychromeController::SetColorsAndSpeed(unsigned char led, unsigned char red, unsigned char green, unsigned char blue)
{
    unsigned char color_speed_pkt[4] = { red, green, blue, active_speed };
    unsigned char select_led_pkt[1]  = { led };
    
    switch(asrock_type)
    {
        case ASROCK_TYPE_ASRLED:
            /*-----------------------------------------------------*\
            | Select LED                                            |
            \*-----------------------------------------------------*/
            if(active_mode != ASRLED_MODE_OFF)
            {
                bus->i2c_smbus_write_block_data(dev, ASROCK_REG_LED_SELECT, 1, select_led_pkt);
                std::this_thread::sleep_for(1ms);
            }

            switch(active_mode)
            {
                /*-----------------------------------------------------*\
                | These modes take 4 bytes in R/G/B/S order             |
                \*-----------------------------------------------------*/
                case ASRLED_MODE_BREATHING:
                case ASRLED_MODE_STROBE:
                case ASRLED_MODE_SPECTRUM_CYCLE:
                    bus->i2c_smbus_write_block_data(dev, active_mode, 4, color_speed_pkt);
                    break;

                /*-----------------------------------------------------*\
                | These modes take 3 bytes in R/G/B order               |
                \*-----------------------------------------------------*/
                default:
                case ASRLED_MODE_STATIC:
                case ASRLED_MODE_MUSIC:
                    bus->i2c_smbus_write_block_data(dev, active_mode, 3, color_speed_pkt);
                    break;

                /*-----------------------------------------------------*\
                | These modes take 1 byte - speed                       |
                \*-----------------------------------------------------*/
                case ASRLED_MODE_RANDOM:
                case ASRLED_MODE_WAVE:
                    bus->i2c_smbus_write_block_data(dev, active_mode, 1, &active_speed);
                    break;

                /*-----------------------------------------------------*\
                | These modes take no bytes                             |
                \*-----------------------------------------------------*/
                case ASRLED_MODE_OFF:
                    break;
            }
            std::this_thread::sleep_for(1ms);
            break;

        case ASROCK_TYPE_POLYCHROME_V1:
            /*-----------------------------------------------------*\
            | Select LED                                            |
            \*-----------------------------------------------------*/
            if(active_mode != ASRLED_MODE_OFF)
            {
                bus->i2c_smbus_write_block_data(dev, ASROCK_REG_LED_SELECT, 1, select_led_pkt);
                std::this_thread::sleep_for(1ms);
            }

            switch(active_mode)
            {
                /*-----------------------------------------------------*\
                | These modes take 4 bytes in R/G/B/S order             |
                \*-----------------------------------------------------*/
                case POLYCHROME_V1_MODE_BREATHING:
                case POLYCHROME_V1_MODE_STROBE:
                case POLYCHROME_V1_MODE_SPECTRUM_CYCLE:
                case POLYCHROME_V1_MODE_SPRING:
                case POLYCHROME_V1_MODE_METEOR:
                case POLYCHROME_V1_MODE_STACK:
                case POLYCHROME_V1_MODE_CRAM:
                case POLYCHROME_V1_MODE_SCAN:
                case POLYCHROME_V1_MODE_NEON:
                case POLYCHROME_V1_MODE_WATER:

                    bus->i2c_smbus_write_block_data(dev, active_mode, 4, color_speed_pkt);
                    break;

                /*-----------------------------------------------------*\
                | These modes take 3 bytes in R/G/B order               |
                \*-----------------------------------------------------*/
                default:
                case POLYCHROME_V1_MODE_STATIC:
                case POLYCHROME_V1_MODE_MUSIC:
                    bus->i2c_smbus_write_block_data(dev, active_mode, 3, color_speed_pkt);
                    break;

                /*-----------------------------------------------------*\
                | These modes take 1 byte - speed                       |
                \*-----------------------------------------------------*/
                case POLYCHROME_V1_MODE_RANDOM:
                case POLYCHROME_V1_MODE_WAVE:
                case POLYCHROME_V1_MODE_RAINBOW:
                    bus->i2c_smbus_write_block_data(dev, active_mode, 1, &active_speed);
                    break;

                /*-----------------------------------------------------*\
                | These modes take no bytes                             |
                \*-----------------------------------------------------*/
                case POLYCHROME_V1_MODE_OFF:
                    break;
            }
            std::this_thread::sleep_for(1ms);
            break;

        case ASROCK_TYPE_POLYCHROME_V2:
            /*-----------------------------------------------------*\
            | Select LED                                            |
            \*-----------------------------------------------------*/
            switch(active_mode)
            {
                case POLYCHROME_V2_MODE_OFF:
                case POLYCHROME_V2_MODE_RAINBOW:
                case POLYCHROME_V2_MODE_SPECTRUM_CYCLE:
                    break;

                default:
                    bus->i2c_smbus_write_block_data(dev, ASROCK_REG_LED_SELECT, 1, select_led_pkt);
                    std::this_thread::sleep_for(1ms);

                    /*-----------------------------------------------------*\
                    | Polychrome firmware always writes color to fixed reg  |
                    \*-----------------------------------------------------*/
                    bus->i2c_smbus_write_block_data(dev, POLYCHROME_V2_REG_COLOR, 3, color_speed_pkt);
                    std::this_thread::sleep_for(1ms);
                    break;
            }
            break;
    }
}

void PolychromeController::SetMode(unsigned char zone,unsigned char mode, unsigned char speed)
{
    unsigned char led_count_pkt[1]  = { 0x00 };
    active_zone                     = zone;
    active_mode                     = mode;
    active_speed                    = speed;

    switch(asrock_type)
    {
        case ASROCK_TYPE_ASRLED:
            bus->i2c_smbus_write_block_data(dev, ASROCK_REG_MODE, 1, &active_mode);
            std::this_thread::sleep_for(1ms);
            break;

        case ASROCK_TYPE_POLYCHROME_V1:
            /*-----------------------------------------------------*\
            | Make sure set all register is set to 0                |
            \*-----------------------------------------------------*/
            bus->i2c_smbus_write_block_data(dev, POLYCHROME_V1_REG_SET_ALL, 1, led_count_pkt);
            std::this_thread::sleep_for(1ms);

            /*-----------------------------------------------------*\
            | Set the zone we are working on                        |
            \*-----------------------------------------------------*/
            bus->i2c_smbus_write_block_data(dev, ASROCK_REG_LED_SELECT, 1, &active_zone);
            std::this_thread::sleep_for(1ms);

            /*-----------------------------------------------------*\
            | Write the mode                                        |
            \*-----------------------------------------------------*/
            bus->i2c_smbus_write_block_data(dev, ASROCK_REG_MODE, 1, &active_mode);
            std::this_thread::sleep_for(1ms);
            break;

        case ASROCK_TYPE_POLYCHROME_V2:
            bus->i2c_smbus_write_block_data(dev, ASROCK_REG_MODE, 1, &active_mode);
            std::this_thread::sleep_for(1ms);

            /*-----------------------------------------------------*\
            | Select a single LED                                   |
            \*-----------------------------------------------------*/
            bus->i2c_smbus_write_block_data(dev, POLYCHROME_V2_REG_LED_COUNT, 0, led_count_pkt);
            std::this_thread::sleep_for(1ms);

            switch(active_mode)
            {
                /*-----------------------------------------------------*\
                | These modes don't take a speed                        |
                \*-----------------------------------------------------*/
                case POLYCHROME_V2_MODE_OFF:
                case POLYCHROME_V2_MODE_STATIC:
                    break;

                /*-----------------------------------------------------*\
                | All other modes, write speed to active mode register  |
                \*-----------------------------------------------------*/
                default:
                    bus->i2c_smbus_write_block_data(dev, active_mode, 1, &speed);
                    std::this_thread::sleep_for(1ms);
                    break;
            }
            break;
    }    
}
