#include "physicalmemory.h"
#include "common/endian.h"
#include "common/smbios.h"

// 7.18
typedef struct [[gnu::packed]] FFSmbiosMemoryDevice {
    FFSmbiosHeader Header;

    // 2.1+
    uint16_t PhysicalMemoryArrayHandle;    // varies
    uint16_t MemoryErrorInformationHandle; // varies
    uint16_t TotalWidth;                   // varies
    uint16_t DataWidth;                    // varies
    uint16_t Size;                         // varies
    uint8_t FormFactor;                    // enum
    uint8_t DeviceSet;                     // varies
    uint8_t DeviceLocator;                 // string
    uint8_t BankLocator;                   // string
    uint8_t MemoryType;                    // enum
    uint16_t TypeDetail;                   // bit field

    // 2.3+
    uint16_t Speed;       // varies
    uint8_t Manufacturer; // string
    uint8_t SerialNumber; // string
    uint8_t AssetTag;     // string
    uint8_t PartNumber;   // string

    // 2.6+
    uint8_t Attributes; // varies

    // 2.7+
    uint32_t ExtendedSize;          // varies
    uint16_t ConfiguredMemorySpeed; // varies

    // 2.8+
    uint16_t MinimumVoltage;    // varies
    uint16_t MaximumVoltage;    // varies
    uint16_t ConfiguredVoltage; // varies

    // 3.2+
    uint8_t MemoryTechnology;                         // varies
    uint16_t MemoryOperatingMode;                     // bit field
    uint8_t FirmwareVersion;                          // string
    uint16_t ModuleManufacturerID;                    // varies
    uint16_t ModuleProductID;                         // varies
    uint16_t MemorySubsystemControllerManufacturerID; // vaies
    uint16_t MemorySubsystemControllerProductID;      // varies
    uint64_t NonVolatileSize;                         // varies
    uint64_t VolatileSize;                            // varies
    uint64_t CacheSize;                               // varies
    uint64_t LogicalSize;                             // varies

    // 3.3+
    uint32_t ExtendedSpeed;           // varies
    uint32_t ExtendedConfiguredSpeed; // varies

    // 3.7+
    uint16_t Pmic0ManufacturerID; // varies
    uint16_t Pmic0RevisionNumber; // varies
    uint16_t RcdManufacturerID;   // varies
    uint16_t RcdRevisionNumber;   // varies
} FFSmbiosMemoryDevice;

static_assert(offsetof(FFSmbiosMemoryDevice, RcdRevisionNumber) == 0x62,
    "FFSmbiosMemoryDevice: Wrong struct alignment");

const char* ffDetectPhysicalMemory(FFPhysicalMemoryOptions* options, FFlist* result) {
    const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable();
    if (!smbiosTable) {
        return "Failed to get SMBIOS data";
    }

    const FFSmbiosMemoryDevice* data = (const FFSmbiosMemoryDevice*) (*smbiosTable)[FF_SMBIOS_TYPE_MEMORY_DEVICE];
    if (!data) {
        return "Memory device is not found in SMBIOS data";
    }

    const FFSmbiosMemoryDevice* endOfTable = (const FFSmbiosMemoryDevice*) (*smbiosTable)[FF_SMBIOS_TYPE_END_OF_TABLE];
    for (; data != endOfTable; data = (const FFSmbiosMemoryDevice*) ffSmbiosNextEntry(&data->Header)) {
        if (data->Header.Type != FF_SMBIOS_TYPE_MEMORY_DEVICE) {
            continue;
        }

        const char* strings = (const char*) data + data->Header.Length;
        uint16_t size = FF_READ_LE(data->Size);
        bool installed = size != 0;

        if (!installed && !options->showEmptySlots) {
            continue;
        }

        FFPhysicalMemoryResult* device = FF_LIST_ADD(FFPhysicalMemoryResult, *result);
        ffStrbufInit(&device->type);
        ffStrbufInit(&device->formFactor);
        ffStrbufInit(&device->locator);
        ffStrbufInit(&device->vendor);
        ffStrbufInit(&device->serial);
        ffStrbufInit(&device->partNumber);
        device->size = 0;
        device->maxSpeed = 0;
        device->runningSpeed = 0;
        device->installed = installed;
        device->ecc = false;

        uint16_t totalWidth = FF_READ_LE(data->TotalWidth);
        uint16_t dataWidth = FF_READ_LE(data->DataWidth);
        if (installed && totalWidth != 0xFFFF && dataWidth != 0xFFFF) {
            device->ecc = totalWidth > dataWidth;
        }

        if (installed && size != 0xFFFF) {
            if (size == 0x7FFF) {
                device->size = (FF_READ_LE(data->ExtendedSize) & ~(1ULL << 31)) * 1024ULL * 1024ULL;
            } else if (size & (1 << 15)) {
                // in kB
                device->size = (size & ~(1ULL << 15)) * 1024ULL;
            } else {
                // in MB
                device->size = size * 1024ULL * 1024ULL;
            }
        }

        // https://github.com/fastfetch-cli/fastfetch/issues/1051#issuecomment-2206687345
        const char* lbank = ffSmbiosLocateString(strings, data->BankLocator);
        const char* ldevice = ffSmbiosLocateString(strings, data->DeviceLocator);
        if (lbank && ldevice) {
            ffStrbufSetF(&device->locator, "%s/%s", lbank, ldevice);
        } else if (lbank) {
            ffStrbufSetS(&device->locator, lbank);
        } else if (ldevice) {
            ffStrbufSetS(&device->locator, ldevice);
        }

        const char* formFactorNames[] = {
            nullptr,               // 0x00 (Placeholder for indexing)
            "Other",            // 0x01
            "Unknown",          // 0x02
            "SIMM",             // 0x03
            "SIP",              // 0x04
            "Chip",             // 0x05
            "DIP",              // 0x06
            "ZIP",              // 0x07
            "Proprietary Card", // 0x08
            "DIMM",             // 0x09
            "TSOP",             // 0x0A
            "Row of chips",     // 0x0B
            "RIMM",             // 0x0C
            "SODIMM",           // 0x0D
            "SRIMM",            // 0x0E
            "FBDIMM",           // 0x0F
            "Die",              // 0x10
            "CAMM",             // 0x11
            "CUDIMM",           // 0x12
            "CSODIMM",          // 0x13
        };
        if (data->FormFactor > 0 && data->FormFactor < ARRAY_SIZE(formFactorNames)) {
            ffStrbufSetS(&device->formFactor, formFactorNames[data->FormFactor]);
        } else {
            ffStrbufSetF(&device->formFactor, "Unknown (%d)", (int) data->FormFactor);
        }

        const char* memoryTypeNames[] = {
            nullptr,                          // 0x00 (Placeholder for indexing)
            "Other",                       // 0x01
            "Unknown",                     // 0x02
            "DRAM",                        // 0x03
            "EDRAM",                       // 0x04
            "VRAM",                        // 0x05
            "SRAM",                        // 0x06
            "RAM",                         // 0x07
            "ROM",                         // 0x08
            "FLASH",                       // 0x09
            "EEPROM",                      // 0x0A
            "FEPROM",                      // 0x0B
            "EPROM",                       // 0x0C
            "CDRAM",                       // 0x0D
            "3DRAM",                       // 0x0E
            "SDRAM",                       // 0x0F
            "SGRAM",                       // 0x10
            "RDRAM",                       // 0x11
            "DDR",                         // 0x12
            "DDR2",                        // 0x13
            "DDR2 FB-DIMM",                // 0x14
            "Reserved",                    // 0x15
            "Reserved",                    // 0x16
            "Reserved",                    // 0x17
            "DDR3",                        // 0x18
            "FBD2",                        // 0x19
            "DDR4",                        // 0x1A
            "LPDDR",                       // 0x1B
            "LPDDR2",                      // 0x1C
            "LPDDR3",                      // 0x1D
            "LPDDR4",                      // 0x1E
            "Logical non-volatile device", // 0x1F
            "HBM",                         // 0x20
            "HBM2",                        // 0x21
            "DDR5",                        // 0x22
            "LPDDR5",                      // 0x23
            "HBM3",                        // 0x24
            "MRDIMM",                      // 0x25
        };
        if (!installed) {
            ffStrbufSetStatic(&device->type, "Empty");
        } else if (data->MemoryType > 0 && data->MemoryType < ARRAY_SIZE(memoryTypeNames)) {
            ffStrbufSetStatic(&device->type, memoryTypeNames[data->MemoryType]);
        } else {
            ffStrbufSetF(&device->type, "Unknown (%d)", (int) data->MemoryType);
        }

        if (installed && data->Header.Length > offsetof(FFSmbiosMemoryDevice, Speed)) // 2.3+
        {
            uint16_t speed = FF_READ_LE(data->Speed);
            if (speed) {
                device->maxSpeed = speed == 0xFFFF ? FF_READ_LE(data->ExtendedSpeed) : speed;
            }

            ffStrbufSetStatic(&device->vendor, ffSmbiosLocateString(strings, data->Manufacturer));
            ffCleanUpSmbiosValue(&device->vendor);
            FFPhysicalMemoryUpdateVendorString(device);

            ffStrbufSetStatic(&device->serial, ffSmbiosLocateString(strings, data->SerialNumber));
            ffCleanUpSmbiosValue(&device->serial);

            ffStrbufSetStatic(&device->partNumber, ffSmbiosLocateString(strings, data->PartNumber));
            ffCleanUpSmbiosValue(&device->partNumber);
        }

        if (installed && data->Header.Length > offsetof(FFSmbiosMemoryDevice, ConfiguredMemorySpeed)) // 2.7+
        {
            uint16_t configuredSpeed = FF_READ_LE(data->ConfiguredMemorySpeed);
            if (configuredSpeed) {
                device->runningSpeed = configuredSpeed == 0xFFFF
                    ? FF_READ_LE(data->ExtendedConfiguredSpeed)
                    : configuredSpeed;
            }
        }
    }

    return nullptr;
}
