/*---------------------------------------------------------*\
| SPDAccessor.cpp                                           |
|                                                           |
|   Access to SPD information on various DIMMs              |
|                                                           |
|   Milan Cermak (krysmanta)                    09 Nov 2024 |
|                                                           |
|   This file is part of the OpenRGB project                |
|   SPDX-License-Identifier: GPL-2.0-or-later               |
\*---------------------------------------------------------*/

#include "DDR4DirectAccessor.h"
#include "DDR5DirectAccessor.h"
#include "LogManager.h"
#include "SPDAccessor.h"

#ifdef __linux__
#include "EE1004Accessor_Linux.h"
#include "SPD5118Accessor_Linux.h"
#endif

using namespace std::chrono_literals;

/*---------------------------------------------------------*\
| Sources for define values:                                |
| - https://en.wikipedia.org/wiki/Serial_presence_detect    |
| - JEDEC DDR5 Serial Presence Detect (SPD): Table of       |
|   contents                                                |
\*---------------------------------------------------------*/
#define BASIC_MEMORY_TYPE_ADDR      (0x02)

#define DDR4_JEDEC_ID_ADDR          (0x140)
#define DDR4_PART_NR_START          (0x149)
#define DDR4_PART_NR_END            (0x15C)
#define DDR4_PART_NR_LEN            (DDR4_PART_NR_END - DDR4_PART_NR_START + 1)
#define DDR4_MANUF_SPECIFIC_START   (0x161)
#define DDR4_MANUF_SPECIFIC_END     (0x17D)
#define DDR4_MANUF_SPECIFIC_LEN     (DDR4_MANUF_SPECIFIC_END - DDR4_MANUF_SPECIFIC_START + 1)

#define DDR5_JEDEC_ID_ADDR          (0x200)
#define DDR5_PART_NR_START          (0x209)
#define DDR5_PART_NR_END            (0x226)
#define DDR5_PART_NR_LEN            (DDR5_PART_NR_END - DDR5_PART_NR_START + 1)
#define DDR5_MANUF_SPECIFIC_START   (0x22B)
#define DDR5_MANUF_SPECIFIC_END     (0x27F)
#define DDR5_MANUF_SPECIFIC_LEN     (DDR5_MANUF_SPECIFIC_END - DDR5_MANUF_SPECIFIC_START + 1)

const char *spd_memory_type_name[] =
{
    "Reserved",
    "FPM",
    "EDO",
    "Nibble",
    "SDR",
    "Multiplex ROM",
    "DDR",
    "DDR",
    "DDR2",
    "FB",
    "FB Probe",
    "DDR3",
    "DDR4",
    "Reserved",
    "DDR4e",
    "LPDDR3",
    "LPDDR4",
    "LPDDR4X",
    "DDR5",
    "LPDDR5"
};

SPDAccessor::SPDAccessor(i2c_smbus_interface *bus, uint8_t spd_addr)
{
    this->bus       = bus;
    this->address   = spd_addr;
}

SPDAccessor::~SPDAccessor()
{
}

SPDAccessor *SPDAccessor::for_memory_type(SPDMemoryType type, i2c_smbus_interface *bus, uint8_t spd_addr)
{
    /*-----------------------------------------------------*\
    | DDR4 can use DDR4DirectAccessor or EE1004Accessor     |
    \*-----------------------------------------------------*/
    if(type == SPD_DDR4_SDRAM)
    {
#ifdef __linux__
        if(EE1004Accessor::isAvailable(bus, spd_addr))
        {
            return(new EE1004Accessor(bus, spd_addr));
        }
#endif
        return(new DDR4DirectAccessor(bus, spd_addr));
    }

    /*-----------------------------------------------------*\
    | DDR5 can use DDR5DirectAccessor or SPD5118Accessor    |
    \*-----------------------------------------------------*/
    if(type == SPD_DDR5_SDRAM)
    {
#ifdef __linux__
        if(SPD5118Accessor::isAvailable(bus, spd_addr))
        {
            return(new SPD5118Accessor(bus, spd_addr));
        }
#endif
        return(new DDR5DirectAccessor(bus, spd_addr));
    }

    return(nullptr);
};

std::string SPDAccessor::read_part_nr_at(uint16_t address, std::size_t len)
{
    std::string part_number;

    for(uint16_t i = 0; i < (uint16_t)len; i++)
    {
        uint16_t spd_addr = address + i;
        part_number += (char)this->at(spd_addr);
    }

    /*-----------------------------------------------------*\
    | Find the true end of string and truncate it to that   |
    | point.  Part number should be padded with 0x20        |
    | (space) for DDR4 (Source: Wikipedia).                 |
    | It may be padded with 0x00 (Source: real-life tests   |
    | on DDR5 memory).                                      |
    | Note: To prevent infinite loop, end_of_string_idx     |
    | MUST be signed.                                       |
    \*-----------------------------------------------------*/
    std::size_t end_of_string_idx = part_number.length();

    for(; end_of_string_idx > 0; end_of_string_idx--)
    {
        if((part_number[end_of_string_idx - 1] != '\0')
        && (part_number[end_of_string_idx - 1] != ' '))
        {
            break;
        }
    }
    part_number = part_number.substr(0, end_of_string_idx + 1);

    return part_number;
}

/*---------------------------------------------------------*\
| Internal implementation for specific memory type.         |
\*---------------------------------------------------------*/

DDR4Accessor::DDR4Accessor(i2c_smbus_interface *bus, uint8_t spd_addr)
  : SPDAccessor(bus, spd_addr)
{
}

DDR4Accessor::~DDR4Accessor()
{
}

SPDMemoryType DDR4Accessor::memory_type()
{
    return((SPDMemoryType)(this->at(BASIC_MEMORY_TYPE_ADDR)));
}

uint16_t DDR4Accessor::jedec_id()
{
    return((this->at(DDR4_JEDEC_ID_ADDR) << 8) + (this->at(DDR4_JEDEC_ID_ADDR+1) & 0x7f) - 1);
}

std::string DDR4Accessor::part_number()
{
    return this->read_part_nr_at(DDR4_PART_NR_START, DDR4_PART_NR_LEN);
}

uint8_t DDR4Accessor::manufacturer_data(uint16_t index)
{
    if(index > DDR4_MANUF_SPECIFIC_LEN-1)
    {
        return 0;
    }
    return this->at(DDR4_MANUF_SPECIFIC_START + index);
}


DDR5Accessor::DDR5Accessor(i2c_smbus_interface *bus, uint8_t spd_addr)
  : SPDAccessor(bus, spd_addr)
{
}

DDR5Accessor::~DDR5Accessor()
{
}

SPDMemoryType DDR5Accessor::memory_type()
{
    return((SPDMemoryType)(this->at(BASIC_MEMORY_TYPE_ADDR)));
}

uint16_t DDR5Accessor::jedec_id()
{
    return((this->at(DDR5_JEDEC_ID_ADDR) << 8) + (this->at(DDR5_JEDEC_ID_ADDR+1) & 0x7f) - 1);
}

std::string DDR5Accessor::part_number()
{
    return this->read_part_nr_at(DDR5_PART_NR_START, DDR5_PART_NR_LEN);
}

uint8_t DDR5Accessor::manufacturer_data(uint16_t index)
{
    if(index > DDR5_MANUF_SPECIFIC_LEN-1)
    {
        return 0;
    }
    return this->at(DDR5_MANUF_SPECIFIC_START + index);
}
