//========================================================================
// GLFW 3.5 Cocoa - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2009-2019 Camilla Löwy <elmindreda@glfw.org>
// Copyright (c) 2012 Torsten Walluhn <tw@mad-cad.net>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//    claim that you wrote the original software. If you use this software
//    in a product, an acknowledgment in the product documentation would
//    be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
//    be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
//    distribution.
//
//========================================================================

#include "internal.h"

#if defined(_GLFW_COCOA)

#include <unistd.h>
#include <ctype.h>
#include <string.h>

#include <mach/mach.h>
#include <mach/mach_error.h>

#include <CoreFoundation/CoreFoundation.h>
#include <Kernel/IOKit/hidsystem/IOHIDUsageTables.h>


// Joystick element information
//
typedef struct _GLFWjoyelementNS
{
    IOHIDElementRef native;
    uint32_t        usage;
    int             index;
    long            minimum;
    long            maximum;

} _GLFWjoyelementNS;


// Returns the value of the specified element of the specified joystick
//
static long getElementValue(_GLFWjoystick* js, _GLFWjoyelementNS* element)
{
    IOHIDValueRef valueRef;
    long value = 0;

    if (js->ns.device)
    {
        if (IOHIDDeviceGetValue(js->ns.device,
                                element->native,
                                &valueRef) == kIOReturnSuccess)
        {
            value = IOHIDValueGetIntegerValue(valueRef);
        }
    }

    return value;
}

// Comparison function for matching the SDL element order
//
static CFComparisonResult compareElements(const void* fp,
                                          const void* sp,
                                          void* user)
{
    const _GLFWjoyelementNS* fe = fp;
    const _GLFWjoyelementNS* se = sp;
    if (fe->usage < se->usage)
        return kCFCompareLessThan;
    if (fe->usage > se->usage)
        return kCFCompareGreaterThan;
    if (fe->index < se->index)
        return kCFCompareLessThan;
    if (fe->index > se->index)
        return kCFCompareGreaterThan;
    return kCFCompareEqualTo;
}

// Removes the specified joystick
//
static void closeJoystick(_GLFWjoystick* js)
{
    _glfwInputJoystick(js, GLFW_DISCONNECTED);

    for (int i = 0;  i < CFArrayGetCount(js->ns.axes);  i++)
        _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.axes, i));
    CFRelease(js->ns.axes);

    for (int i = 0;  i < CFArrayGetCount(js->ns.buttons);  i++)
        _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.buttons, i));
    CFRelease(js->ns.buttons);

    for (int i = 0;  i < CFArrayGetCount(js->ns.hats);  i++)
        _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.hats, i));
    CFRelease(js->ns.hats);

    _glfwFreeJoystick(js);
}

// Callback for user-initiated joystick addition
//
static void matchCallback(void* context,
                          IOReturn result,
                          void* sender,
                          IOHIDDeviceRef device)
{
    int jid;
    char name[256];
    char guid[33];
    CFTypeRef property;
    uint32_t vendor = 0, product = 0, version = 0;
    _GLFWjoystick* js;
    CFMutableArrayRef axes, buttons, hats;

    for (jid = 0;  jid <= GLFW_JOYSTICK_LAST;  jid++)
    {
        if (_glfw.joysticks[jid].ns.device == device)
            return;
    }

    CFArrayRef elements =
        IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone);

    // It is reportedly possible for this to fail on macOS 13 Ventura
    // if the application does not have input monitoring permissions
    if (!elements)
        return;

    axes    = CFArrayCreateMutable(NULL, 0, NULL);
    buttons = CFArrayCreateMutable(NULL, 0, NULL);
    hats    = CFArrayCreateMutable(NULL, 0, NULL);

    property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
    if (property)
    {
        CFStringGetCString(property,
                           name,
                           sizeof(name),
                           kCFStringEncodingUTF8);
    }
    else
        strncpy(name, "Unknown", sizeof(name));

    property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey));
    if (property)
        CFNumberGetValue(property, kCFNumberSInt32Type, &vendor);

    property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey));
    if (property)
        CFNumberGetValue(property, kCFNumberSInt32Type, &product);

    property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVersionNumberKey));
    if (property)
        CFNumberGetValue(property, kCFNumberSInt32Type, &version);

    // Generate a joystick GUID that matches the SDL 2.0.5+ one
    if (vendor && product)
    {
        sprintf(guid, "03000000%02x%02x0000%02x%02x0000%02x%02x0000",
                (uint8_t) vendor, (uint8_t) (vendor >> 8),
                (uint8_t) product, (uint8_t) (product >> 8),
                (uint8_t) version, (uint8_t) (version >> 8));
    }
    else
    {
        sprintf(guid, "05000000%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x00",
                name[0], name[1], name[2], name[3],
                name[4], name[5], name[6], name[7],
                name[8], name[9], name[10]);
    }

    for (CFIndex i = 0;  i < CFArrayGetCount(elements);  i++)
    {
        IOHIDElementRef native = (IOHIDElementRef)
            CFArrayGetValueAtIndex(elements, i);
        if (CFGetTypeID(native) != IOHIDElementGetTypeID())
            continue;

        const IOHIDElementType type = IOHIDElementGetType(native);
        if ((type != kIOHIDElementTypeInput_Axis) &&
            (type != kIOHIDElementTypeInput_Button) &&
            (type != kIOHIDElementTypeInput_Misc))
        {
            continue;
        }

        CFMutableArrayRef target = NULL;

        const uint32_t usage = IOHIDElementGetUsage(native);
        const uint32_t page = IOHIDElementGetUsagePage(native);
        if (page == kHIDPage_GenericDesktop)
        {
            switch (usage)
            {
                case kHIDUsage_GD_X:
                case kHIDUsage_GD_Y:
                case kHIDUsage_GD_Z:
                case kHIDUsage_GD_Rx:
                case kHIDUsage_GD_Ry:
                case kHIDUsage_GD_Rz:
                case kHIDUsage_GD_Slider:
                case kHIDUsage_GD_Dial:
                case kHIDUsage_GD_Wheel:
                    target = axes;
                    break;
                case kHIDUsage_GD_Hatswitch:
                    target = hats;
                    break;
                case kHIDUsage_GD_DPadUp:
                case kHIDUsage_GD_DPadRight:
                case kHIDUsage_GD_DPadDown:
                case kHIDUsage_GD_DPadLeft:
                case kHIDUsage_GD_SystemMainMenu:
                case kHIDUsage_GD_Select:
                case kHIDUsage_GD_Start:
                    target = buttons;
                    break;
            }
        }
        else if (page == kHIDPage_Simulation)
        {
            switch (usage)
            {
                case kHIDUsage_Sim_Accelerator:
                case kHIDUsage_Sim_Brake:
                case kHIDUsage_Sim_Throttle:
                case kHIDUsage_Sim_Rudder:
                case kHIDUsage_Sim_Steering:
                    target = axes;
                    break;
            }
        }
        else if (page == kHIDPage_Button || page == kHIDPage_Consumer)
            target = buttons;

        if (target)
        {
            _GLFWjoyelementNS* element = _glfw_calloc(1, sizeof(_GLFWjoyelementNS));
            element->native  = native;
            element->usage   = usage;
            element->index   = (int) CFArrayGetCount(target);
            element->minimum = IOHIDElementGetLogicalMin(native);
            element->maximum = IOHIDElementGetLogicalMax(native);
            CFArrayAppendValue(target, element);
        }
    }

    CFRelease(elements);

    CFArraySortValues(axes, CFRangeMake(0, CFArrayGetCount(axes)),
                      compareElements, NULL);
    CFArraySortValues(buttons, CFRangeMake(0, CFArrayGetCount(buttons)),
                      compareElements, NULL);
    CFArraySortValues(hats, CFRangeMake(0, CFArrayGetCount(hats)),
                      compareElements, NULL);

    js = _glfwAllocJoystick(name, guid,
                            (int) CFArrayGetCount(axes),
                            (int) CFArrayGetCount(buttons),
                            (int) CFArrayGetCount(hats));

    js->ns.device  = device;
    js->ns.axes    = axes;
    js->ns.buttons = buttons;
    js->ns.hats    = hats;

    _glfwInputJoystick(js, GLFW_CONNECTED);
}

// Callback for user-initiated joystick removal
//
static void removeCallback(void* context,
                           IOReturn result,
                           void* sender,
                           IOHIDDeviceRef device)
{
    for (int jid = 0;  jid <= GLFW_JOYSTICK_LAST;  jid++)
    {
        if (_glfw.joysticks[jid].connected && _glfw.joysticks[jid].ns.device == device)
        {
            closeJoystick(&_glfw.joysticks[jid]);
            break;
        }
    }
}


//////////////////////////////////////////////////////////////////////////
//////                       GLFW platform API                      //////
//////////////////////////////////////////////////////////////////////////

GLFWbool _glfwInitJoysticksCocoa(void)
{
    CFMutableArrayRef matching;
    const long usages[] =
    {
        kHIDUsage_GD_Joystick,
        kHIDUsage_GD_GamePad,
        kHIDUsage_GD_MultiAxisController
    };

    _glfw.ns.hidManager = IOHIDManagerCreate(kCFAllocatorDefault,
                                             kIOHIDOptionsTypeNone);

    matching = CFArrayCreateMutable(kCFAllocatorDefault,
                                    0,
                                    &kCFTypeArrayCallBacks);
    if (!matching)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create array");
        return GLFW_FALSE;
    }

    for (size_t i = 0;  i < sizeof(usages) / sizeof(long);  i++)
    {
        const long page = kHIDPage_GenericDesktop;

        CFMutableDictionaryRef dict =
            CFDictionaryCreateMutable(kCFAllocatorDefault,
                                      0,
                                      &kCFTypeDictionaryKeyCallBacks,
                                      &kCFTypeDictionaryValueCallBacks);
        if (!dict)
            continue;

        CFNumberRef pageRef = CFNumberCreate(kCFAllocatorDefault,
                                             kCFNumberLongType,
                                             &page);
        CFNumberRef usageRef = CFNumberCreate(kCFAllocatorDefault,
                                              kCFNumberLongType,
                                              &usages[i]);
        if (pageRef && usageRef)
        {
            CFDictionarySetValue(dict,
                                 CFSTR(kIOHIDDeviceUsagePageKey),
                                 pageRef);
            CFDictionarySetValue(dict,
                                 CFSTR(kIOHIDDeviceUsageKey),
                                 usageRef);
            CFArrayAppendValue(matching, dict);
        }

        if (pageRef)
            CFRelease(pageRef);
        if (usageRef)
            CFRelease(usageRef);

        CFRelease(dict);
    }

    IOHIDManagerSetDeviceMatchingMultiple(_glfw.ns.hidManager, matching);
    CFRelease(matching);

    IOHIDManagerRegisterDeviceMatchingCallback(_glfw.ns.hidManager,
                                               &matchCallback, NULL);
    IOHIDManagerRegisterDeviceRemovalCallback(_glfw.ns.hidManager,
                                              &removeCallback, NULL);
    IOHIDManagerScheduleWithRunLoop(_glfw.ns.hidManager,
                                    CFRunLoopGetMain(),
                                    kCFRunLoopDefaultMode);
    IOHIDManagerOpen(_glfw.ns.hidManager, kIOHIDOptionsTypeNone);

    // Execute the run loop once in order to register any initially-attached
    // joysticks
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
    return GLFW_TRUE;
}

void _glfwTerminateJoysticksCocoa(void)
{
    for (int jid = 0;  jid <= GLFW_JOYSTICK_LAST;  jid++)
    {
        if (_glfw.joysticks[jid].connected)
            closeJoystick(&_glfw.joysticks[jid]);
    }

    if (_glfw.ns.hidManager)
    {
        CFRelease(_glfw.ns.hidManager);
        _glfw.ns.hidManager = NULL;
    }
}


GLFWbool _glfwPollJoystickCocoa(_GLFWjoystick* js, int mode)
{
    if (mode & _GLFW_POLL_AXES)
    {
        for (CFIndex i = 0;  i < CFArrayGetCount(js->ns.axes);  i++)
        {
            _GLFWjoyelementNS* axis = (_GLFWjoyelementNS*)
                CFArrayGetValueAtIndex(js->ns.axes, i);

            const long raw = getElementValue(js, axis);
            // Perform auto calibration
            if (raw < axis->minimum)
                axis->minimum = raw;
            if (raw > axis->maximum)
                axis->maximum = raw;

            const long size = axis->maximum - axis->minimum;
            if (size == 0)
                _glfwInputJoystickAxis(js, (int) i, 0.f);
            else
            {
                const float value = (2.f * (raw - axis->minimum) / size) - 1.f;
                _glfwInputJoystickAxis(js, (int) i, value);
            }
        }
    }

    if (mode & _GLFW_POLL_BUTTONS)
    {
        for (CFIndex i = 0;  i < CFArrayGetCount(js->ns.buttons);  i++)
        {
            _GLFWjoyelementNS* button = (_GLFWjoyelementNS*)
                CFArrayGetValueAtIndex(js->ns.buttons, i);
            const char value = getElementValue(js, button) - button->minimum;
            const int state = (value > 0) ? GLFW_PRESS : GLFW_RELEASE;
            _glfwInputJoystickButton(js, (int) i, state);
        }

        for (CFIndex i = 0;  i < CFArrayGetCount(js->ns.hats);  i++)
        {
            const int states[9] =
            {
                GLFW_HAT_UP,
                GLFW_HAT_RIGHT_UP,
                GLFW_HAT_RIGHT,
                GLFW_HAT_RIGHT_DOWN,
                GLFW_HAT_DOWN,
                GLFW_HAT_LEFT_DOWN,
                GLFW_HAT_LEFT,
                GLFW_HAT_LEFT_UP,
                GLFW_HAT_CENTERED
            };

            _GLFWjoyelementNS* hat = (_GLFWjoyelementNS*)
                CFArrayGetValueAtIndex(js->ns.hats, i);
            long state = getElementValue(js, hat) - hat->minimum;
            if (state < 0 || state > 8)
                state = 8;

            _glfwInputJoystickHat(js, (int) i, states[state]);
        }
    }

    return js->connected;
}

const char* _glfwGetMappingNameCocoa(void)
{
    return "Mac OS X";
}

void _glfwUpdateGamepadGUIDCocoa(char* guid)
{
    if ((strncmp(guid + 4, "000000000000", 12) == 0) &&
        (strncmp(guid + 20, "000000000000", 12) == 0))
    {
        char original[33];
        strncpy(original, guid, sizeof(original) - 1);
        sprintf(guid, "03000000%.4s0000%.4s000000000000",
                original, original + 16);
    }
}

#endif // _GLFW_COCOA

