// setfan.c -- get / set fan speed on Mac mini M4 (Mac16,10) via SMC
// Compile: clang -framework IOKit -o setfan setfan.c
// Based on reverse-engineered SMC dumps; uses two-step read / write
// Works on macOS Tahoe (and later) for Apple Silicon Mac mini.

#include <IOKit/IOKitLib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// SMC structures (same layout as known-good getfan.c)
typedef struct { char major, minor, build, reserved[1]; UInt16 release; } SMCVersion;
typedef struct { UInt16 version, length; UInt32 cpuPLimit, gpuPLimit, memPLimit; } SMCPLimitData;
typedef struct { UInt32 dataSize, dataType; UInt8 dataAttributes; } SMCKeyInfoData;

typedef struct {
    UInt32            key;
    SMCVersion        vers;
    SMCPLimitData     pLimitData;
    SMCKeyInfoData    keyInfo;
    UInt8             result;
    UInt8             status;
    UInt8             data8;
    UInt32            data32;
    UInt8             bytes[32];
} SMCParamStruct;

enum {
    kSMCHandleYPCEvent   = 2,
    kSMCGetKeyInfo       = 9,
    kSMCReadKey          = 5,
    kSMCWriteKey         = 6,
};

// Convert 4-char key to UInt32
static UInt32 s2u(const char *s) {
    UInt32 v = 0;
    for (int i = 0; i < 4; i++) v = (v << 8) | (unsigned char)s[i];
    return v;
}

// Low-level call wrapper
static int smc_call(io_connect_t conn, SMCParamStruct *in, SMCParamStruct *out) {
    size_t sz = sizeof(SMCParamStruct);
    return IOConnectCallStructMethod(conn, kSMCHandleYPCEvent, in, sz, out, &sz);
}

// Get key info (size + type) – returns a copy of keyInfo
static int get_key_info(io_connect_t conn, const char *key, SMCKeyInfoData *out_ki) {
    SMCParamStruct p = {0};
    p.data8 = kSMCGetKeyInfo;
    p.key   = s2u(key);
    if (smc_call(conn, &p, &p) != kIOReturnSuccess || p.result)
        return -1;
    *out_ki = p.keyInfo;
    return 0;
}

// Read a 4-byte float from a key (e.g., F0Ac, F0Tg)
static int read_float(io_connect_t conn, const char *key, float *val) {
    SMCKeyInfoData ki;
    if (get_key_info(conn, key, &ki) != 0) return -1;
    if (ki.dataSize != 4) return -2;   // expecting flt

    SMCParamStruct p = {0};
    p.data8   = kSMCReadKey;
    p.key     = s2u(key);
    p.keyInfo = ki;
    if (smc_call(conn, &p, &p) != kIOReturnSuccess || p.result) return -3;

    memcpy(val, p.bytes, 4);
    return 0;
}

// Read a 1-byte UI8 from a key (e.g., FNum, F0Md)
static int read_ui8(io_connect_t conn, const char *key, UInt8 *val) {
    SMCKeyInfoData ki;
    if (get_key_info(conn, key, &ki) != 0) return -1;
    if (ki.dataSize != 1) return -2;   // expecting ui8

    SMCParamStruct p = {0};
    p.data8   = kSMCReadKey;
    p.key     = s2u(key);
    p.keyInfo = ki;
    if (smc_call(conn, &p, &p) != kIOReturnSuccess || p.result) return -3;

    *val = p.bytes[0];
    return 0;
}

// Generic write – supply bytes and length
static int write_bytes(io_connect_t conn, const char *key, const void *data, size_t len) {
    SMCKeyInfoData ki;
    if (get_key_info(conn, key, &ki) != 0) return -1;
    if (ki.dataSize != len) return -2;   // size mismatch

    SMCParamStruct p = {0};
    p.data8   = kSMCWriteKey;
    p.key     = s2u(key);
    p.keyInfo = ki;
    memcpy(p.bytes, data, len);

    if (smc_call(conn, &p, &p) != kIOReturnSuccess || p.result) return -3;
    return 0;
}

static int write_ui8(io_connect_t conn, const char *key, UInt8 val) {
    return write_bytes(conn, key, &val, 1);
}

static int write_float(io_connect_t conn, const char *key, float val) {
    return write_bytes(conn, key, &val, 4);
}

// Open AppleSMC service
static io_connect_t open_smc(void) {
    io_service_t srv = IOServiceGetMatchingService(kIOMainPortDefault,
                                                   IOServiceMatching("AppleSMC"));
    if (!srv) return 0;

    io_connect_t conn;
    if (IOServiceOpen(srv, mach_task_self(), 0, &conn)) {
        IOObjectRelease(srv);
        return 0;
    }
    IOObjectRelease(srv);
    return conn;
}

int main(int argc, char **argv) {
    // --version / --help
    if (argc == 2) {
        if (strcmp(argv[1], "--version") == 0) {
            printf("setfan 1.0\n");
            return 0;
        }
        if (strcmp(argv[1], "--help") == 0) {
            printf("Usage: setfan [RPM|auto]\n"
                   "  no args    print current fan speed\n"
                   "  RPM        set fan target speed in RPM (manual mode)\n"
                   "  auto       set fan control to automatic\n"
                   "  --help     print this help\n"
                   "  --version  print version\n");
            return 0;
        }
    }

    io_connect_t conn = open_smc();
    if (!conn) {
        fprintf(stderr, "Error: Cannot open AppleSMC\n");
        return 1;
    }

    // ---- no args: print current fan speed
    if (argc == 1) {
        UInt8 nfans = 0;
        if (read_ui8(conn, "FNum", &nfans) != 0) {
            fprintf(stderr, "Error: Cannot read fan count\n");
            IOServiceClose(conn);
            return 1;
        }

        for (UInt8 i = 0; i < nfans; i++) {
            char key[5];
            snprintf(key, sizeof(key), "F%dAc", i);
            float rpm = 0;
            if (read_float(conn, key, &rpm) == 0)
                printf("Fan %d: %.0f RPM\n", i, rpm);
            else
                printf("Fan %d: read failed\n", i);
        }
        IOServiceClose(conn);
        return 0;
    }

    // ---- one argument: "auto" or numeric RPM
    if (argc != 2) {
        fprintf(stderr, "Error: Wrong number of arguments\n");
        IOServiceClose(conn);
        return 1;
    }

    UInt8 nfans = 0;
    if (read_ui8(conn, "FNum", &nfans) != 0) {
        fprintf(stderr, "Error: Cannot read fan count\n");
        IOServiceClose(conn);
        return 1;
    }
    if (nfans == 0) {
        fprintf(stderr, "Error: No fans found\n");
        IOServiceClose(conn);
        return 1;
    }

    // ---- "auto"
    if (strcmp(argv[1], "auto") == 0) {
        int ok = 1;
        for (UInt8 i = 0; i < nfans; i++) {
            char key[5];
            snprintf(key, sizeof(key), "F%dMd", i);
            if (write_ui8(conn, key, 0) != 0) {
                fprintf(stderr, "Error: Failed to set auto mode for fan %d\n", i);
                ok = 0;
            }
        }
        if (ok) printf("Fan control set to automatic.\n");
        IOServiceClose(conn);
        return ok ? 0 : 1;
    }

    // ---- numeric RPM
    char *end;
    float rpm = strtof(argv[1], &end);
    if (end == argv[1] || *end != '\0') {
        fprintf(stderr, "Error: Invalid RPM value \"%s\"\n", argv[1]);
        IOServiceClose(conn);
        return 1;
    }
    if (rpm < 0) {
        fprintf(stderr, "Error: RPM must be >= 0\n");
        IOServiceClose(conn);
        return 1;
    }

    int ok = 1;
    for (UInt8 i = 0; i < nfans; i++) {
        char key[5];

        // Enable manual mode
        snprintf(key, sizeof(key), "F%dMd", i);
        if (write_ui8(conn, key, 1) != 0) {
            fprintf(stderr, "Error: Failed to enable manual mode for fan %d\n", i);
            ok = 0;
            continue;
        }

        // Set target RPM
        snprintf(key, sizeof(key), "F%dTg", i);
        if (write_float(conn, key, rpm) != 0) {
            fprintf(stderr, "Error: Failed to set target RPM for fan %d\n", i);
            ok = 0;
            // Don't try to revert here; manual mode remains on
        }
    }

    if (ok) printf("Fan set to %.0f RPM (manual)\n", rpm);
    IOServiceClose(conn);
    return ok ? 0 : 1;
}

