/*---------------------------------------------------------*\
| EVGAGPUv3Controller.cpp                                   |
|                                                           |
|   Driver for EVGA V3 (Ampere) GPU                         |
|                                                           |
|   TheRogueZeta                                15 Jul 2021 |
|                                                           |
|   This file is part of the OpenRGB project                |
|   SPDX-License-Identifier: GPL-2.0-or-later               |
\*---------------------------------------------------------*/

#include "EVGAGPUv3Controller.h"
#include "LogManager.h"

EVGAGPUv3Controller::EVGAGPUv3Controller(i2c_smbus_interface* bus, evga_dev_id dev, std::string dev_name)
{
    this->bus   = bus;
    this->dev   = dev;
    this->name  = dev_name;
}

EVGAGPUv3Controller::~EVGAGPUv3Controller()
{

}

std::string EVGAGPUv3Controller::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 EVGAGPUv3Controller::GetDeviceName()
{
    return(name);
}

void EVGAGPUv3Controller::GetDeviceModes()
{
    LOG_DEBUG("[%s] Getting Zone and LED count from HW", name.c_str());
    uint8_t data_pkt[I2C_SMBUS_BLOCK_MAX] = {};

    uint8_t result = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_MODE, 10, data_pkt);
    if (result == 10)
    {
        for(uint8_t zone = 0 ; zone < 4; zone ++)
        {
            zone_modes[zone] = data_pkt[zone + 1];
            zone_led_count[zone] = data_pkt[zone + 5];
            LOG_DEBUG("[%s] Zone %1d LED count: %02d, mode: %02d", name.c_str(), zone + 1, zone_led_count[zone], zone_modes[zone]);
        }
        zone_sync = data_pkt[9];
        LOG_DEBUG("[%s] Zone Sync is %1d", name.c_str(), zone_sync);
    }
    else
    {
            LOG_DEBUG("[%s] Invalid block read result: %02d", name.c_str(), result);
            memset(zone_led_count, 0, sizeof(zone_led_count));
            memset(zone_modes, 0, sizeof(zone_modes));
    }
    initCard();
}

std::string EVGAGPUv3Controller::GetFWVersion()
{
    return(fwVersion);
}

std::string EVGAGPUv3Controller::ReadFWVersion()
{
    LOG_TRACE("[%s] Getting FW from HW", name.c_str());
    uint8_t         data_pkt[I2C_SMBUS_BLOCK_MAX] = {};
    std::string     return_string = "";
    char            version[10];

    uint8_t         result = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_FIRMWARE, 6, data_pkt);
    if (result == 6)
    {
        uint8_t     major = data_pkt[4];
        uint8_t     minor = data_pkt[5];

        snprintf(version, 10, "1.%02d.%02d", major, minor);
        return_string.append(version);
        LOG_TRACE("[%s] Firmware %s", name.c_str(), version);
        fwVersion = return_string;
        return(return_string);
    }
    else
    {
        return "";
    }
}

uint8_t EVGAGPUv3Controller::GetZoneMode(uint8_t zone)
{
    return zone_modes[zone];
}

EVGAv3_config EVGAGPUv3Controller::GetZoneConfig(uint8_t zone, uint8_t mode)
{
    EVGAv3_config               zone_config;
    u16_to_u8                   speed16;
    bool                        readFail = false;

    zone_config.brightness      = EVGA_GPU_V3_BRIGHTNESS_DEFAULT;
    zone_config.direction       = 0;
    zone_config.numberOfColors  = 0;
    zone_config.speed           = EVGA_GPU_V3_SPEED_GENERIC_NORMAL;

    LOG_DEBUG("[%s] Retriving Zone %1d config for mode %1d from HW", name.c_str(), zone, mode);
    uint8_t data_pkt[I2C_SMBUS_BLOCK_MAX] = {};

    switch (mode)
    {
        case EVGA_GPU_V3_MODE_STATIC:
            {
                uint8_t result = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + zone, 5, data_pkt);
                if (result == 5)
                {
                    //Load data
                    zone_config.brightness      = data_pkt[1];
                    uint8_t red                 = data_pkt[2];
                    uint8_t green               = data_pkt[3];
                    uint8_t blue                = data_pkt[4];
                    zone_config.colors[0]       = ToRGBColor(red, green, blue);
                    zone_config.numberOfColors  = 1;
                    zone_config.speed           = 0;
                    zone_config.direction       = 0;
                }
                else
                {
                    readFail = true;
                }
            }
            break;

        case EVGA_GPU_V3_MODE_BREATHING:
            {
                uint8_t result = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_BREATHING + zone, 10, data_pkt);
                if (result == 10)
                {
                    //Load data
                    zone_config.brightness      = data_pkt[1];
                    uint8_t red1                = data_pkt[2];
                    uint8_t green1              = data_pkt[3];
                    uint8_t blue1               = data_pkt[4];
                    zone_config.colors[0]       = ToRGBColor(red1, green1, blue1);
                    uint8_t red2                = data_pkt[5];
                    uint8_t green2              = data_pkt[6];
                    uint8_t blue2               = data_pkt[7];
                    zone_config.colors[1]       = ToRGBColor(red2, green2, blue2);
                    zone_config.numberOfColors= (zone_config.colors[1] != 0 ) ? 2 : 1 ;
                    speed16.lsb                 = data_pkt[8];
                    speed16.msb                 = data_pkt[9];
                    zone_config.speed           = speed16.u16;
                    zone_config.direction       = 0;
                }
                else
                {
                    readFail = true;
                }
            }
            break;

        case EVGA_GPU_V3_MODE_RAINBOW:
        case EVGA_GPU_V3_MODE_RAINBOW_WAVE:
        case EVGA_GPU_V3_MODE_STAR:
            {
                uint8_t result = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + ((mode -1) * 4) + zone, 4, data_pkt);
                if (result == 4)
                {
                    //Load data
                    zone_config.brightness      = data_pkt[1];
                    zone_config.numberOfColors  = 0;
                    speed16.lsb                 = data_pkt[2];
                    speed16.msb                 = data_pkt[3];
                    zone_config.speed           = speed16.u16;
                }
                else
                {
                    readFail = true;
                }
            }
            break;

        case EVGA_GPU_V3_MODE_WAVE:
            {
                uint8_t result = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + ((mode -1) * 4) + zone, 7, data_pkt);
                if (result == 7)
                {
                    //Load data
                    zone_config.brightness      = data_pkt[1];
                    uint8_t red                 = data_pkt[2];
                    uint8_t green               = data_pkt[3];
                    uint8_t blue                = data_pkt[4];
                    zone_config.colors[0]       = ToRGBColor(red, green, blue);
                    zone_config.numberOfColors  = 1;
                    speed16.lsb                 = data_pkt[5];
                    speed16.msb                 = data_pkt[6];
                    zone_config.speed           = speed16.u16;
                }
                else
                {
                    readFail = true;
                }
            }
            break;

        case EVGA_GPU_V3_MODE_COLOR_CYCLE:
        case EVGA_GPU_V3_MODE_COLOR_STACK:
            {
                uint8_t cd_pkt[5];

                if(mode == EVGA_GPU_V3_MODE_COLOR_STACK)
                {
                    uint8_t color_count = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_COLOR_STACK_COLOR_COUNT, 5, cd_pkt);

                    if(color_count == 5)
                    {
                        zone_config.numberOfColors = cd_pkt[zone + 1];
                    }
                    else
                    {
                        readFail = true;
                    }

                    uint8_t direction = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_COLOR_STACK_DIRECTION, 5, cd_pkt);
                    if(direction == 5)
                    {
                        zone_config.direction = cd_pkt[zone + 1];
                    }
                    else
                    {
                        readFail = true;
                    }

                }
                else
                {
                    uint8_t color_count = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_COLOR_CYCLE_COUNT, 5, cd_pkt);

                    if(color_count == 5)
                    {
                        zone_config.numberOfColors = cd_pkt[zone + 1];
                    }
                    else
                    {
                        readFail = true;
                    }
                }

                uint8_t result = bus->i2c_smbus_read_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + ((mode -1) * 4) + zone, 31, data_pkt);
                if (result == 31)
                {
                    zone_config.brightness      = data_pkt[1];
                    for(uint8_t color_index = 0; color_index < zone_config.numberOfColors; color_index++)
                    {
                        uint8_t red             = data_pkt[(color_index * 4) + 2];
                        uint8_t green           = data_pkt[(color_index * 4) + 3];
                        uint8_t blue            = data_pkt[(color_index * 4) + 4];
                        zone_config.colors[color_index] = ToRGBColor(red, green, blue);
                    }
                    speed16.lsb                 = data_pkt[29];
                    speed16.msb                 = data_pkt[30];
                    zone_config.speed           = speed16.u16;
                }
                else
                {
                    readFail = true;
                }
            }
            break;

        default:
            break;
    }

    if(readFail == false)
    {
        LOG_TRACE("[%s] Zone %1d Brightness: 0x%02X, Colors: %1d, Speed: 0x%04X, Direction %1d.", name.c_str(), zone, zone_config.brightness, zone_config.numberOfColors, zone_config.speed, zone_config.direction);
        for(uint8_t color_index = 0; color_index < zone_config.numberOfColors; color_index++)
        {
            LOG_TRACE("[%s] Color Index [%2d]: 0x%06X", name.c_str(), color_index, zone_config.colors[color_index]);
        }
        LOG_DEBUG("[%s] Done loading Zone %1d configuration from HW", name.c_str(), zone);
    }
    else
    {
        zone_config.direction       = 0;
        zone_config.numberOfColors  = 0;
        for(uint8_t i = 0; i < 7; i++)
        {
            zone_config.colors[i] = 0;
        }
        LOG_DEBUG("[%s] Failed while loading Zone %1d configuration from HW", name.c_str(), zone);
    }
    return zone_config;
}

void EVGAGPUv3Controller::initCard()
{
    // This command needs to be sent before the card will respond to OpenRGB commands
    // NvAPI_I2CWriteEx: Dev: 0x2D RegSize: 0x01 Reg: 0xB2 Size: 0x05 Data: 0x04 0xC6 0xEB 0xEA 0x15
    uint8_t data_pkt[5] = {0x04, 0xC6, 0xEB, 0xEA, 0x15};
    bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_ENABLE, sizeof(data_pkt), data_pkt);
    LOG_TRACE("[%s] Sending SW int packet", name.c_str());
    return;
}

void EVGAGPUv3Controller::SaveConfig()
{
    LOG_DEBUG("[%s] Sending save packet", name.c_str());

    //NvAPI_I2CWriteEx: Dev: 0x2D RegSize: 0x01 Reg: 0x90 Size: 0x05 Data: 0x04 0x9E 0xEB 0x00 0x90 //Sent on close of PX1
    uint8_t data_pkt[5] = {0x04, 0x9E, 0xEB, 0x00, 0x90};

    bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_SAVE, sizeof(data_pkt), data_pkt);
    return;
}

void EVGAGPUv3Controller::ResizeARGB(uint8_t newSize)
{
    if(newSize < EVGAGPUV3_LEDS_MIN)
    {
        newSize = EVGAGPUV3_LEDS_MIN;
    }
    else if(newSize > EVGAGPUV3_LEDS_MAX)
    {
        newSize = EVGAGPUV3_LEDS_MAX;
    }

    LOG_DEBUG("[%s] Resizing ARGB header with %02d size", name.c_str(), newSize);
    uint8_t data_pkt[EVGAGPUV3_MODE_PACKET_SIZE] = { 0x09, EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT,
                                                           EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, newSize, EVGAGPUV3_INIT};

    bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_MODE, sizeof(data_pkt), data_pkt);
    return;
}

void EVGAGPUv3Controller::SetAllModes(uint8_t zone_0_mode, uint8_t zone_1_mode,uint8_t zone_2_mode,uint8_t zone_3_mode, bool sync)
{
    uint8_t     mode_pkt[EVGAGPUV3_MODE_PACKET_SIZE]    = { EVGAGPUV3_MODE_PACKET_SIZE - 1,
                                                        EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT,
                                                        EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, 0x0 };

    // Hack to keep card in sync after power loss (standby) without requireing a rescan.
    // To be replaced with code that calls this once upon system resume once OpenRGB
    // knows about system power states.
    initCard();

    // Keep zone_modes in sync
    zone_modes[0] = zone_0_mode;
    zone_modes[1] = zone_1_mode;
    zone_modes[2] = zone_2_mode;
    zone_modes[3] = zone_3_mode;

    // Prep packet
    mode_pkt[1] = zone_0_mode;
    mode_pkt[2] = zone_1_mode;
    mode_pkt[3] = zone_2_mode;
    mode_pkt[4] = zone_3_mode;
    mode_pkt[9] = sync;

    bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_MODE, EVGAGPUV3_MODE_PACKET_SIZE, mode_pkt);
    //LOG_TRACE("[%s] Setting all zones to mode: %02d, %2d, %2d, %2d, zone sync %1d.", name.c_str(), zone_0_mode, zone_1_mode, zone_2_mode, zone_3_mode, sync);
}

void EVGAGPUv3Controller::SetZoneMode(uint8_t zone, uint8_t mode)
{
    uint8_t     mode_pkt[EVGAGPUV3_MODE_PACKET_SIZE]    = { EVGAGPUV3_MODE_PACKET_SIZE - 1,
                                                        EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT,
                                                        EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, 0x0 };

    // Hack to keep card in sync after power loss (standby) without requireing a rescan.
    // To be replaced with code that calls this once upon system resume once OpenRGB
    // knows about system power states.
    initCard();

    // Keep zone_modes in sync
    zone_modes[zone] = mode;
    // Prep packet
    mode_pkt[zone + 1] = mode;

    bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_MODE, EVGAGPUV3_MODE_PACKET_SIZE, mode_pkt);
    //LOG_TRACE("[%s] Setting individual zone %1d to mode %02d", name.c_str(), zone, mode);
}

void EVGAGPUv3Controller::SetZone(uint8_t zone, uint8_t mode, EVGAv3_config zone_config)
{
    std::string mode_name;
    u16_to_u8   speed16                                 = { (uint16_t) zone_config.speed };

    switch (mode)
    {
    case EVGA_GPU_V3_MODE_OFF:
        break;

    case EVGA_GPU_V3_MODE_STATIC:
        {
            uint8_t zone_pkt[5]     = {EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT, EVGAGPUV3_INIT};

            zone_pkt[0]             = sizeof(zone_pkt) - 1;
            zone_pkt[1]             = zone_config.brightness;
            zone_pkt[2]             = RGBGetRValue(zone_config.colors[0]);
            zone_pkt[3]             = RGBGetGValue(zone_config.colors[0]);
            zone_pkt[4]             = RGBGetBValue(zone_config.colors[0]);

            bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + zone, sizeof(zone_pkt), zone_pkt);
        }
        break;

    case EVGA_GPU_V3_MODE_BREATHING:
        {
            uint8_t zone_pkt[10];
            memset(zone_pkt, EVGAGPUV3_INIT, sizeof(zone_pkt));

            zone_pkt[0]             = sizeof(zone_pkt) - 1;
            zone_pkt[1]             = zone_config.brightness;
            zone_pkt[2]             = RGBGetRValue(zone_config.colors[0]);
            zone_pkt[3]             = RGBGetGValue(zone_config.colors[0]);
            zone_pkt[4]             = RGBGetBValue(zone_config.colors[0]);
            if(zone_config.numberOfColors == 1)
            {
                zone_config.colors[1] = 0;
            }
            zone_pkt[5]             = RGBGetRValue(zone_config.colors[1]);
            zone_pkt[6]             = RGBGetGValue(zone_config.colors[1]);
            zone_pkt[7]             = RGBGetBValue(zone_config.colors[1]);
            zone_pkt[8]             = speed16.lsb;
            zone_pkt[9]             = speed16.msb;

            bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + ((mode -1) * 4) + zone, sizeof(zone_pkt), zone_pkt);
        }
        break;

    case EVGA_GPU_V3_MODE_RAINBOW:
    case EVGA_GPU_V3_MODE_RAINBOW_WAVE:
    case EVGA_GPU_V3_MODE_STAR:
        {
            uint8_t zone_pkt[4];
            memset(zone_pkt, EVGAGPUV3_INIT, sizeof(zone_pkt));

            zone_pkt[0]             = sizeof(zone_pkt) - 1;
            zone_pkt[1]             = zone_config.brightness;
            zone_pkt[2]             = speed16.lsb;
            zone_pkt[3]             = speed16.msb;

            bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + ((mode -1) * 4) + zone, sizeof(zone_pkt), zone_pkt);
        }
        break;

    case EVGA_GPU_V3_MODE_WAVE:
        {
            uint8_t zone_pkt[7];
            memset(zone_pkt, EVGAGPUV3_INIT, sizeof(zone_pkt));

            zone_pkt[0]             = sizeof(zone_pkt) - 1;
            zone_pkt[1]             = zone_config.brightness;
            zone_pkt[2]             = RGBGetRValue(zone_config.colors[0]);
            zone_pkt[3]             = RGBGetGValue(zone_config.colors[0]);
            zone_pkt[4]             = RGBGetBValue(zone_config.colors[0]);
            zone_pkt[5]             = speed16.lsb;
            zone_pkt[6]             = speed16.msb;

            bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + ((mode -1) * 4) + zone, sizeof(zone_pkt), zone_pkt);
        }
        break;

    case EVGA_GPU_V3_MODE_COLOR_CYCLE:
    case EVGA_GPU_V3_MODE_COLOR_STACK:
        {
            uint8_t zone_pkt[31];;
            memset(zone_pkt, EVGAGPUV3_INIT, sizeof(zone_pkt));
            uint8_t color_cnt_pkt[5];
            memset(color_cnt_pkt, EVGAGPUV3_INIT, sizeof(color_cnt_pkt));

            // Zone packet construction
            zone_pkt[0] = sizeof(zone_pkt) - 1;
            for(uint8_t color_index = 0; color_index < zone_config.numberOfColors; color_index++)
            {
                zone_pkt[1 + color_index * 4] = zone_config.brightness;
                zone_pkt[2 + color_index * 4] = RGBGetRValue(zone_config.colors[color_index]);
                zone_pkt[3 + color_index * 4] = RGBGetGValue(zone_config.colors[color_index]);
                zone_pkt[4 + color_index * 4] = RGBGetBValue(zone_config.colors[color_index]);
            }
            zone_pkt[29] = speed16.lsb;
            zone_pkt[30] = speed16.msb;

            // Color Count packet construction
            color_cnt_pkt[0]                = sizeof(color_cnt_pkt) - 1;
            color_cnt_pkt[zone+1]           = zone_config.numberOfColors;

            if(mode == EVGA_GPU_V3_MODE_COLOR_STACK)
            {
                uint8_t direction_pkt[5];
                memset(direction_pkt, EVGAGPUV3_INIT, sizeof(direction_pkt));
                // Direction packet construction
                direction_pkt[0]            = sizeof(direction_pkt) - 1;
                direction_pkt[zone+1]       = zone_config.direction;

                bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + ((mode -1) * 4) + zone, sizeof(zone_pkt), zone_pkt);
                bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_COLOR_STACK_COLOR_COUNT, sizeof(color_cnt_pkt), color_cnt_pkt);
                bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_COLOR_STACK_DIRECTION, sizeof(direction_pkt), direction_pkt);
            }
            else
            {
                bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_STATIC + ((mode -1) * 4) + zone, sizeof(zone_pkt), zone_pkt);
                bus->i2c_smbus_write_i2c_block_data(dev, EVGA_GPU_V3_REG_COLOR_CYCLE_COUNT, sizeof(color_cnt_pkt), color_cnt_pkt);
            }
        }
        break;
    default:
        {
            LOG_TRACE("[%s] Mode %02d not found", name.c_str(), mode);
        }
        break;
    }
}
