#include "smc_temps.h"
#include "common/apple/cf_helpers.h"
#include "common/strutil.h"

#include <stdint.h>
#include <IOKit/IOKitLib.h>

static const char kSmcCmdReadBytes = 5;
static const char kSmcCmdReadKeyInfo = 9;
static const uint32_t kKernelIndexSmc = 2;

typedef struct
{
    char major;
    char minor;
    char build;
    char reserved[1];
    uint16_t release;
} SmcKeyData_vers_t;

typedef struct
{
    uint16_t version;
    uint16_t length;
    uint32_t cpuPLimit;
    uint32_t gpuPLimit;
    uint32_t memPLimit;
} SmcKeyData_pLimitData_t;

typedef struct
{
    uint32_t dataSize;
    uint32_t dataType;
    char dataAttributes;
} SmcKeyData_keyInfo_t;

typedef unsigned char SmcBytes_t[32];

typedef struct
{
    uint32_t key;
    SmcKeyData_vers_t vers;
    SmcKeyData_pLimitData_t pLimitData;
    SmcKeyData_keyInfo_t keyInfo;
    char result;
    char status;
    char data8;
    uint32_t data32;
    SmcBytes_t bytes;
} SmcKeyData_t;

typedef char UInt32Char_t[5];

typedef struct
{
    UInt32Char_t key;
    uint32_t dataSize;
    UInt32Char_t dataType;
    SmcBytes_t bytes;
} SmcVal_t;

static uint32_t smcStrtoul(const char* str, int size, int base) {
    uint32_t total = 0;

    for (int i = 0; i < size; i++) {
        if (base == 16) {
            total += (uint32_t) (str[i] << (size - 1 - i) * 8);
        } else {
            total += (uint32_t) ((unsigned char) (str[i]) << (size - 1 - i) * 8);
        }
    }
    return total;
}

static void smcUltostr(char* str, uint32_t val) {
    str[0] = (char) (val >> 24);
    str[1] = (char) (val >> 16);
    str[2] = (char) (val >> 8);
    str[3] = (char) val;
    str[4] = '\0';
}

static const char* smcCall(io_connect_t conn, uint32_t selector, SmcKeyData_t* inputStructure, SmcKeyData_t* outputStructure) {
    size_t size = sizeof(SmcKeyData_t);

    if (IOConnectCallStructMethod(conn, selector, inputStructure, size, outputStructure, &size) != kIOReturnSuccess) {
        return "IOConnectCallStructMethod(conn) failed";
    }
    return NULL;
}

// Provides key info, using a cache to dramatically improve the energy impact of smcFanControl
static const char* smcGetKeyInfo(io_connect_t conn, const uint32_t key, SmcKeyData_keyInfo_t* key_info) {
    SmcKeyData_t inputStructure = { 0 };
    SmcKeyData_t outputStructure = { 0 };

    inputStructure.key = key;
    inputStructure.data8 = kSmcCmdReadKeyInfo;

    const char* error = smcCall(conn, kKernelIndexSmc, &inputStructure, &outputStructure);
    if (error) {
        return error;
    }

    *key_info = outputStructure.keyInfo;
    return NULL;
}

static const char* smcReadSmcVal(io_connect_t conn, const UInt32Char_t key, SmcVal_t* val) {
    SmcKeyData_t inputStructure = { 0 };
    SmcKeyData_t outputStructure = { 0 };

    inputStructure.key = smcStrtoul(key, 4, 16);
    strcpy(val->key, key);

    const char* error = smcGetKeyInfo(conn, inputStructure.key, &outputStructure.keyInfo);
    if (error) {
        return error;
    }

    val->dataSize = outputStructure.keyInfo.dataSize;
    smcUltostr(val->dataType, outputStructure.keyInfo.dataType);
    inputStructure.keyInfo.dataSize = val->dataSize;
    inputStructure.data8 = kSmcCmdReadBytes;

    error = smcCall(conn, kKernelIndexSmc, &inputStructure, &outputStructure);
    if (error) {
        return error;
    }

    memcpy(val->bytes, outputStructure.bytes, sizeof(outputStructure.bytes));

    return NULL;
}

static const char* smcOpen(io_connect_t* conn) {
    FF_IOOBJECT_AUTO_RELEASE io_object_t device = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("AppleSMC"));
    if (!device) {
        return "No SMC device found";
    }

    if (IOServiceOpen(device, mach_task_self(), 0, conn) != kIOReturnSuccess) {
        return "IOServiceOpen() failed";
    }

    return NULL;
}

static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, double* value) {
    SmcVal_t val = { 0 };
    const char* error = smcReadSmcVal(conn, key, &val);
    if (error != NULL) {
        return error;
    }
    if (val.dataSize == 0) {
        return "Empty SMC result";
    }

    switch (val.dataType[0]) {
        case 'u': // unsigned integer types
            if (val.dataType[1] == 'i') {
                switch (val.dataSize) {
                    case 1:
                        *value = *(uint8_t*) (val.bytes);
                        break;
                    case 2:
                        *value = ntohs(*(uint16_t*) (val.bytes));
                        break;
                    case 4:
                        *value = ntohl(*(uint32_t*) (val.bytes));
                        break;
                    case 8:
                        *value = (double) ntohll(*(uint64_t*) (val.bytes));
                        break;
                    default:
                        return "Unsupported SMC unsigned integer data size";
                }
            } else {
                return "Unsupported SMC unsigned data type";
            }
            break;

        case 'f': // floating point types
            if (ffStrEquals(val.dataType, "flt ") && val.dataSize == 4) {
                *value = *(float*) (val.bytes);
            } else if (val.dataType[1] == 'p' && val.dataSize == 2) // fixed point types
            {
                if (ffStrEquals(val.dataType, "fp1f")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 32768.0;
                } else if (ffStrEquals(val.dataType, "fp4c")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 4096.0;
                } else if (ffStrEquals(val.dataType, "fp5b")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 2048.0;
                } else if (ffStrEquals(val.dataType, "fp6a")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 1024.0;
                } else if (ffStrEquals(val.dataType, "fp79")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 512.0;
                } else if (ffStrEquals(val.dataType, "fp88")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 256.0;
                } else if (ffStrEquals(val.dataType, "fpa6")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 64.0;
                } else if (ffStrEquals(val.dataType, "fpc4")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 16.0;
                } else if (ffStrEquals(val.dataType, "fpe2")) {
                    *value = ntohs(*(uint16_t*) (val.bytes)) / 4.0;
                } else {
                    return "Unsupported SMC floating point data type";
                }
            } else {
                return "Unsupported SMC floating point data type";
            }
            break;

        case 's': // signed integer types
            if (val.dataType[1] == 'i') {
                switch (val.dataSize) {
                    case 1:
                        *value = *(int8_t*) (val.bytes);
                        break;
                    case 2:
                        *value = ntohs(*(int16_t*) (val.bytes));
                        break;
                    case 4:
                        *value = ntohl(*(int32_t*) (val.bytes));
                        break;
                    case 8:
                        *value = (double) ntohll(*(int64_t*) (val.bytes));
                        break;
                    default:
                        return "Unsupported SMC signed integer data size";
                }
            } else if (val.dataType[1] == 'p' && val.dataSize == 2) // signed fixed point types
            {
                if (ffStrEquals(val.dataType, "sp1e")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 16384.0;
                } else if (ffStrEquals(val.dataType, "sp3c")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 4096.0;
                } else if (ffStrEquals(val.dataType, "sp4b")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 2048.0;
                } else if (ffStrEquals(val.dataType, "sp5a")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 1024.0;
                } else if (ffStrEquals(val.dataType, "sp69")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 512.0;
                } else if (ffStrEquals(val.dataType, "sp78")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 256.0;
                } else if (ffStrEquals(val.dataType, "sp87")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 128.0;
                } else if (ffStrEquals(val.dataType, "sp96")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 64.0;
                } else if (ffStrEquals(val.dataType, "spb4")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 16.0;
                } else if (ffStrEquals(val.dataType, "spf0")) {
                    *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 1.0;
                } else {
                    return "Unsupported SMC signed integer data type";
                }
            } else {
                return "Unsupported SMC signed data type";
            }
            break;

        case '{': // special types like pwm
            if (ffStrEquals(val.dataType, "{pwm") && val.dataSize == 2) {
                *value = (double) ntohs(*(uint16_t*) (val.bytes)) * 100 / 65536.0;
            } else {
                return "Unsupported SMC special data type";
            }
            break;

        default:
            return "Unsupported SMC data type";
    }
    return NULL;
}

static bool detectTemp(io_connect_t conn, const char* sensor, double* sum) {
    double temp = 0;
    const char* error = smcReadValue(conn, sensor, &temp);
    if (error) {
        return false;
    }
    // https://github.com/exelban/stats/blob/14e29c4d60229c363cca9c9d25c30c87b7870830/Modules/Sensors/readers.swift#L124
    if (temp < 10 || temp > 120) {
        return false;
    }
    *sum += temp;
    return true;
}

static io_connect_t conn;

const char* ffDetectSmcSpecificTemp(const char* sensor, double* result) {
    if (!conn) {
        if (smcOpen(&conn) != NULL) {
            conn = (io_connect_t) -1;
        }
    }
    if (conn == (io_connect_t) -1) {
        return "Could not open SMC connection";
    }

    if (!detectTemp(conn, sensor, result)) {
        return "Could not read SMC temperature";
    }

    return NULL;
}

const char* ffDetectSmcTemps(enum FFTempType type, double* result) {
    if (!conn) {
        if (smcOpen(&conn) != NULL) {
            conn = (io_connect_t) -1;
        }
    }
    if (conn == (io_connect_t) -1) {
        return "Could not open SMC connection";
    }

    uint32_t count = 0;
    *result = 0;

    // https://github.com/exelban/stats/blob/master/Modules/Sensors/values.swift
    switch (type) {
        case FF_TEMP_CPU_X64:
            count += detectTemp(conn, "TC0D", result); // CPU diode
            count += detectTemp(conn, "TC0E", result); // CPU diode virtual
            count += detectTemp(conn, "TC0F", result); // CPU diode filtered
            count += detectTemp(conn, "TC0P", result); // CPU proximity
            break;

        case FF_TEMP_CPU_M1X:
            count += detectTemp(conn, "Tp09", result); // CPU efficient core 1
            count += detectTemp(conn, "Tp0T", result); // CPU efficient core 2

            count += detectTemp(conn, "Tp01", result); // CPU performance core 1
            count += detectTemp(conn, "Tp05", result); // CPU performance core 2
            count += detectTemp(conn, "Tp0D", result); // CPU performance core 3
            count += detectTemp(conn, "Tp0H", result); // CPU performance core 4
            count += detectTemp(conn, "Tp0L", result); // CPU performance core 5
            count += detectTemp(conn, "Tp0P", result); // CPU performance core 6
            count += detectTemp(conn, "Tp0X", result); // CPU performance core 7
            count += detectTemp(conn, "Tp0b", result); // CPU performance core 8
            break;

        case FF_TEMP_CPU_M2X:
            count += detectTemp(conn, "Tp1h", result); // CPU efficiency core 1
            count += detectTemp(conn, "Tp1t", result); // CPU efficiency core 2
            count += detectTemp(conn, "Tp1p", result); // CPU efficiency core 3
            count += detectTemp(conn, "Tp1l", result); // CPU efficiency core 4

            count += detectTemp(conn, "Tp01", result); // CPU performance core 1
            count += detectTemp(conn, "Tp05", result); // CPU performance core 2
            count += detectTemp(conn, "Tp09", result); // CPU performance core 3
            count += detectTemp(conn, "Tp0D", result); // CPU performance core 4
            count += detectTemp(conn, "Tp0X", result); // CPU performance core 5
            count += detectTemp(conn, "Tp0b", result); // CPU performance core 6
            count += detectTemp(conn, "Tp0f", result); // CPU performance core 7
            count += detectTemp(conn, "Tp0j", result); // CPU performance core 8
            break;

        case FF_TEMP_CPU_M3X:
            count += detectTemp(conn, "Te05", result); // CPU efficiency core 1
            count += detectTemp(conn, "Te0L", result); // CPU efficiency core 2
            count += detectTemp(conn, "Te0P", result); // CPU efficiency core 3
            count += detectTemp(conn, "Te0S", result); // CPU efficiency core 4
            count += detectTemp(conn, "Tf04", result); // CPU performance core 1
            count += detectTemp(conn, "Tf09", result); // CPU performance core 2
            count += detectTemp(conn, "Tf0A", result); // CPU performance core 3
            count += detectTemp(conn, "Tf0B", result); // CPU performance core 4
            count += detectTemp(conn, "Tf0D", result); // CPU performance core 5
            count += detectTemp(conn, "Tf0E", result); // CPU performance core 6
            count += detectTemp(conn, "Tf44", result); // CPU performance core 7
            count += detectTemp(conn, "Tf49", result); // CPU performance core 8
            count += detectTemp(conn, "Tf4A", result); // CPU performance core 9
            count += detectTemp(conn, "Tf4B", result); // CPU performance core 10
            count += detectTemp(conn, "Tf4D", result); // CPU performance core 11
            count += detectTemp(conn, "Tf4E", result); // CPU performance core 12
            break;

        case FF_TEMP_CPU_M4X:
            count += detectTemp(conn, "Te05", result); // CPU efficiency core 1
            count += detectTemp(conn, "Te0S", result); // CPU efficiency core 2
            count += detectTemp(conn, "Te09", result); // CPU efficiency core 3
            count += detectTemp(conn, "Te0H", result); // CPU efficiency core 4
            count += detectTemp(conn, "Tp01", result); // CPU performance core 1
            count += detectTemp(conn, "Tp05", result); // CPU performance core 2
            count += detectTemp(conn, "Tp09", result); // CPU performance core 3
            count += detectTemp(conn, "Tp0D", result); // CPU performance core 4
            count += detectTemp(conn, "Tp0V", result); // CPU performance core 5
            count += detectTemp(conn, "Tp0Y", result); // CPU performance core 6
            count += detectTemp(conn, "Tp0b", result); // CPU performance core 7
            count += detectTemp(conn, "Tp0e", result); // CPU performance core 8
            break;

        case FF_TEMP_GPU_INTEL:
            count += detectTemp(conn, "TCGC", result); // GPU Intel Graphics
            goto gpu_unknown;

        case FF_TEMP_GPU_AMD:
            count += detectTemp(conn, "TGDD", result); // GPU AMD Radeon
            goto gpu_unknown;

        case FF_TEMP_GPU_UNKNOWN: // Nvidia?
        gpu_unknown:
            count += detectTemp(conn, "TG0D", result); // GPU diode
            count += detectTemp(conn, "TG0P", result); // GPU proximity
            break;

        case FF_TEMP_GPU_M1X:
            count += detectTemp(conn, "Tg05", result); // GPU 1
            count += detectTemp(conn, "Tg0D", result); // GPU 2
            count += detectTemp(conn, "Tg0L", result); // GPU 3
            count += detectTemp(conn, "Tg0T", result); // GPU 4
            break;

        case FF_TEMP_GPU_M2X:
            count += detectTemp(conn, "Tg0f", result); // GPU 1
            count += detectTemp(conn, "Tg0j", result); // GPU 2
            break;

        case FF_TEMP_GPU_M3X:
            count += detectTemp(conn, "Tf14", result); // GPU 1
            count += detectTemp(conn, "Tf18", result); // GPU 2
            count += detectTemp(conn, "Tf19", result); // GPU 3
            count += detectTemp(conn, "Tf1A", result); // GPU 4
            count += detectTemp(conn, "Tf24", result); // GPU 5
            count += detectTemp(conn, "Tf28", result); // GPU 6
            count += detectTemp(conn, "Tf29", result); // GPU 7
            count += detectTemp(conn, "Tf2A", result); // GPU 8
            break;

        case FF_TEMP_GPU_M4X:
            count += detectTemp(conn, "Tg0G", result); // GPU 1 (Basic)
            count += detectTemp(conn, "Tg0H", result); // GPU 2 (Basic)
            count += detectTemp(conn, "Tg1U", result); // GPU 1 (Pro / Max)
            count += detectTemp(conn, "Tg1k", result); // GPU 2 (Pro / Max)
            count += detectTemp(conn, "Tg0K", result); // GPU 3
            count += detectTemp(conn, "Tg0L", result); // GPU 4
            count += detectTemp(conn, "Tg0d", result); // GPU 5
            count += detectTemp(conn, "Tg0e", result); // GPU 6
            count += detectTemp(conn, "Tg0j", result); // GPU 7
            count += detectTemp(conn, "Tg0k", result); // GPU 8
            break;

        case FF_TEMP_BATTERY:
            count += detectTemp(conn, "TB1T", result); // Battery
            count += detectTemp(conn, "TB2T", result); // Battery
            break;

        case FF_TEMP_MEMORY:
            count += detectTemp(conn, "Tm02", result); // Memory 1
            count += detectTemp(conn, "Tm06", result); // Memory 2
            count += detectTemp(conn, "Tm08", result); // Memory 3
            count += detectTemp(conn, "Tm09", result); // Memory 4
            break;
    }

    if (count == 0) {
        return "No temperatures detected";
    }

    *result /= count;

    return NULL;
}
