#include "brightness.h"
#include "detection/displayserver/displayserver.h"
#include "common/apple/cf_helpers.h"
#include "common/edidHelper.h"
#include "common/time.h"

#include <CoreGraphics/CoreGraphics.h>

// DDC/CI
#ifdef __aarch64__
typedef CFTypeRef IOAVServiceRef;
extern IOAVServiceRef IOAVServiceCreate(CFAllocatorRef allocator) FF_A_WEAK_IMPORT;
extern IOAVServiceRef IOAVServiceCreateWithService(CFAllocatorRef allocator, io_service_t service) FF_A_WEAK_IMPORT;
extern IOReturn IOAVServiceCopyEDID(IOAVServiceRef service, CFDataRef* x2) FF_A_WEAK_IMPORT;
extern IOReturn IOAVServiceReadI2C(IOAVServiceRef service, uint32_t chipAddress, uint32_t offset, void* outputBuffer, uint32_t outputBufferSize) FF_A_WEAK_IMPORT;
extern IOReturn IOAVServiceWriteI2C(IOAVServiceRef service, uint32_t chipAddress, uint32_t dataAddress, void* inputBuffer, uint32_t inputBufferSize) FF_A_WEAK_IMPORT;
#else
    // DDC/CI (Intel)
    #include <IOKit/IOKitLib.h>
    #include <IOKit/graphics/IOGraphicsLib.h>
    #include <IOKit/i2c/IOI2CInterface.h>
extern void CGSServiceForDisplayNumber(CGDirectDisplayID display, io_service_t* service) FF_A_WEAK_IMPORT;
#endif

// ACPI
extern int DisplayServicesGetBrightness(CGDirectDisplayID display, float* brightness) FF_A_WEAK_IMPORT;

// Works for internal display
static const char* detectWithDisplayServices(const FFDisplayServerResult* displayServer, FFlist* result) {
    if (DisplayServicesGetBrightness == NULL) {
        return "DisplayServices function DisplayServicesGetBrightness is not available";
    }

    FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) {
        if (display->type == FF_DISPLAY_TYPE_BUILTIN || display->type == FF_DISPLAY_TYPE_UNKNOWN) {
            float value;
            if (DisplayServicesGetBrightness((CGDirectDisplayID) display->id, &value) == kCGErrorSuccess) {
                FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
                brightness->current = value;
                brightness->max = 1;
                brightness->min = 0;
                ffStrbufInitCopy(&brightness->name, &display->name);
                brightness->builtin = true;
            }
        }
    }

    return NULL;
}

#ifdef __aarch64__
// https://github.com/waydabber/m1ddc
// Works for Apple Silicon and USB-C adapter connection ( but not HTMI )
static const char* detectWithDdcci(FF_A_UNUSED const FFDisplayServerResult* displayServer, FFBrightnessOptions* options, FFlist* result) {
    if (!IOAVServiceCreate || !IOAVServiceReadI2C || !IOAVServiceWriteI2C) {
        return "IOAVService is not available";
    }

    FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL;
    if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("DCPAVServiceProxy"), &iterator) != kIOReturnSuccess) {
        return "IOServiceGetMatchingServices() failed";
    }

    io_registry_entry_t registryEntry;
    while ((registryEntry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
        FF_CFTYPE_AUTO_RELEASE IOAVServiceRef service = NULL;
        {
            FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryAv = registryEntry;

            FF_CFTYPE_AUTO_RELEASE CFBooleanRef IOAVServiceUserInterfaceSupported = IORegistryEntryCreateCFProperty(entryAv, CFSTR("IOAVServiceUserInterfaceSupported"), kCFAllocatorDefault, kNilOptions);
            if (IOAVServiceUserInterfaceSupported && !CFBooleanGetValue(IOAVServiceUserInterfaceSupported)) {
                // IOAVServiceCreateWithService won't work
                continue;
            }

            FF_CFTYPE_AUTO_RELEASE CFStringRef location = IORegistryEntryCreateCFProperty(entryAv, CFSTR("Location"), kCFAllocatorDefault, kNilOptions);
            if (location && CFStringCompare(location, CFSTR("Embedded"), 0) == 0) {
                // Builtin display should be handled by DisplayServices
                continue;
            }

            service = IOAVServiceCreateWithService(kCFAllocatorDefault, (io_service_t) registryEntry);
            if (!service) {
                continue;
            }
        }

        {
            uint8_t i2cIn[4] = { FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE };
            i2cIn[3] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2];

            for (uint32_t i = 0; i < 2; ++i) {
                IOAVServiceWriteI2C(service, FF_DDC_CI_ADDR, FF_DDC_CI_VCP_COMMAND, i2cIn, ARRAY_SIZE(i2cIn));
                ffTimeSleep(options->ddcciSleep);
            }
        }

        uint8_t i2cOut[12] = {};
        if (IOAVServiceReadI2C(service, FF_DDC_CI_ADDR, FF_DDC_CI_VCP_COMMAND, i2cOut, ARRAY_SIZE(i2cOut)) == KERN_SUCCESS) {
            if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) {
                continue;
            }

            uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9];
            uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7];

            FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
            brightness->max = max;
            brightness->min = 0;
            brightness->current = current;
            ffStrbufInit(&brightness->name);
            brightness->builtin = false;

            uint8_t edid[128] = {};
            if (IOAVServiceReadI2C(service, 0x50, 0x00, edid, ARRAY_SIZE(edid)) == KERN_SUCCESS) {
                ffEdidGetName(edid, &brightness->name);
            }
        }
    }

    return NULL;
}
#else
static IOOptionBits getSupportedTransactionType(void) {
    FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL;

    if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceNameMatching("IOFramebufferI2CInterface"), &iterator) != KERN_SUCCESS) {
        return kIOI2CNoTransactionType;
    }

    io_registry_entry_t registryEntry;
    while ((registryEntry = IOIteratorNext(iterator)) != MACH_PORT_NULL) {
        FF_IOOBJECT_AUTO_RELEASE io_service_t io_service = registryEntry;
        FF_CFTYPE_AUTO_RELEASE CFNumberRef IOI2CTransactionTypes = IORegistryEntryCreateCFProperty(io_service, CFSTR(kIOI2CTransactionTypesKey), kCFAllocatorDefault, kNilOptions);

        if (IOI2CTransactionTypes) {
            int64_t types = 0;
            ffCfNumGetInt64(IOI2CTransactionTypes, &types);

            if (types) {
                if ((1 << kIOI2CDDCciReplyTransactionType) & (uint64_t) types) {
                    return kIOI2CDDCciReplyTransactionType;
                }
                if ((1 << kIOI2CSimpleTransactionType) & (uint64_t) types) {
                    return kIOI2CSimpleTransactionType;
                }
            }
        }
        break;
    }

    return kIOI2CNoTransactionType;
}

static const char* detectWithDdcci(const FFDisplayServerResult* displayServer, FFBrightnessOptions* options, FFlist* result) {
    if (!CGSServiceForDisplayNumber) {
        return "CGSServiceForDisplayNumber is not available";
    }
    IOOptionBits transactionType = getSupportedTransactionType();
    if (transactionType == kIOI2CNoTransactionType) {
        return "No supported IOI2C transaction type found";
    }

    FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) {
        if (display->type == FF_DISPLAY_TYPE_EXTERNAL) {
            FF_IOOBJECT_AUTO_RELEASE io_service_t framebuffer = IO_OBJECT_NULL;
            CGSServiceForDisplayNumber((CGDirectDisplayID) display->id, &framebuffer);
            if (framebuffer == IO_OBJECT_NULL) {
                continue;
            }

            IOItemCount count;
            if (IOFBGetI2CInterfaceCount(framebuffer, &count) != KERN_SUCCESS || count == 0) {
                continue;
            }

            for (IOItemCount bus = 0; bus < count; ++bus) {
                FF_IOOBJECT_AUTO_RELEASE io_service_t interface = IO_OBJECT_NULL;
                if (IOFBCopyI2CInterfaceForBus(framebuffer, bus, &interface) != KERN_SUCCESS) {
                    continue;
                }

                uint8_t i2cOut[12] = {};
                IOI2CConnectRef connect = NULL;
                if (IOI2CInterfaceOpen(interface, kNilOptions, &connect) != KERN_SUCCESS) {
                    continue;
                }

                uint8_t i2cIn[] = { FF_DDC_CI_VCP_COMMAND, FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE, 0 };
                i2cIn[4] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2] ^ i2cIn[3];

                IOI2CRequest request = {
                    .commFlags = kNilOptions,
                    .sendAddress = FF_DDC_CI_WRITE_ADDR,
                    .sendTransactionType = kIOI2CSimpleTransactionType,
                    .sendBuffer = (vm_address_t) i2cIn,
                    .sendBytes = ARRAY_SIZE(i2cIn),
                    .minReplyDelay = options->ddcciSleep * 1000ULL,
                    .replyAddress = FF_DDC_CI_READ_ADDR,
                    .replySubAddress = FF_DDC_CI_VCP_COMMAND,
                    .replyTransactionType = transactionType,
                    .replyBytes = ARRAY_SIZE(i2cOut),
                    .replyBuffer = (vm_address_t) i2cOut,
                };
                IOReturn ret = IOI2CSendRequest(connect, kNilOptions, &request);
                IOI2CInterfaceClose(connect, kNilOptions);

                if (ret != KERN_SUCCESS || request.result != kIOReturnSuccess || request.replyBytes < 10) {
                    continue;
                }
                if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) {
                    continue;
                }

                uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9];
                uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7];

                FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
                brightness->max = max;
                brightness->min = 0;
                brightness->current = current;
                ffStrbufInitCopy(&brightness->name, &display->name);
                brightness->builtin = false;

                break;
            }
        }
    }

    return NULL;
}
#endif

const char* ffDetectBrightness(FFBrightnessOptions* options, FFlist* result) {
    const FFDisplayServerResult* displayServer = ffConnectDisplayServer();

    detectWithDisplayServices(displayServer, result);

    if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && displayServer->displays.length > result->length) {
        detectWithDdcci(displayServer, options, result);
    }

    return NULL;
}
