/*  Bus like function for mac HID devices
 *
 * Copyright 2016 CodeWeavers, Aric Stewart
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "config.h"
#include "wine/port.h"

#include <stdarg.h>

#if defined(HAVE_IOKIT_HID_IOHIDLIB_H)
#define DWORD UInt32
#define LPDWORD UInt32*
#define LONG SInt32
#define LPLONG SInt32*
#define E_PENDING __carbon_E_PENDING
#define ULONG __carbon_ULONG
#define E_INVALIDARG __carbon_E_INVALIDARG
#define E_OUTOFMEMORY __carbon_E_OUTOFMEMORY
#define E_HANDLE __carbon_E_HANDLE
#define E_ACCESSDENIED __carbon_E_ACCESSDENIED
#define E_UNEXPECTED __carbon_E_UNEXPECTED
#define E_FAIL __carbon_E_FAIL
#define E_ABORT __carbon_E_ABORT
#define E_POINTER __carbon_E_POINTER
#define E_NOINTERFACE __carbon_E_NOINTERFACE
#define E_NOTIMPL __carbon_E_NOTIMPL
#define S_FALSE __carbon_S_FALSE
#define S_OK __carbon_S_OK
#define HRESULT_FACILITY __carbon_HRESULT_FACILITY
#define IS_ERROR __carbon_IS_ERROR
#define FAILED __carbon_FAILED
#define SUCCEEDED __carbon_SUCCEEDED
#define MAKE_HRESULT __carbon_MAKE_HRESULT
#define HRESULT __carbon_HRESULT
#define STDMETHODCALLTYPE __carbon_STDMETHODCALLTYPE
#define PAGE_SHIFT __carbon_PAGE_SHIFT
#include <IOKit/IOKitLib.h>
#include <IOKit/hid/IOHIDLib.h>
#undef ULONG
#undef E_INVALIDARG
#undef E_OUTOFMEMORY
#undef E_HANDLE
#undef E_ACCESSDENIED
#undef E_UNEXPECTED
#undef E_FAIL
#undef E_ABORT
#undef E_POINTER
#undef E_NOINTERFACE
#undef E_NOTIMPL
#undef S_FALSE
#undef S_OK
#undef HRESULT_FACILITY
#undef IS_ERROR
#undef FAILED
#undef SUCCEEDED
#undef MAKE_HRESULT
#undef HRESULT
#undef STDMETHODCALLTYPE
#undef DWORD
#undef LPDWORD
#undef LONG
#undef LPLONG
#undef E_PENDING
#undef PAGE_SHIFT
#endif /* HAVE_IOKIT_HID_IOHIDLIB_H */

#define NONAMELESSUNION

#include "ntstatus.h"
#define WIN32_NO_STATUS
#include "windef.h"
#include "winbase.h"
#include "winternl.h"
#include "winioctl.h"
#include "ddk/wdm.h"
#include "ddk/hidtypes.h"
#include "wine/debug.h"

#include "bus.h"

WINE_DEFAULT_DEBUG_CHANNEL(plugplay);
#ifdef HAVE_IOHIDMANAGERCREATE

static IOHIDManagerRef hid_manager;
static CFRunLoopRef run_loop;
static HANDLE run_loop_handle;

static const WCHAR busidW[] = {'I','O','H','I','D',0};

struct platform_private
{
    IOHIDDeviceRef device;
    uint8_t *buffer;
};

static inline struct platform_private *impl_from_DEVICE_OBJECT(DEVICE_OBJECT *device)
{
    return (struct platform_private *)get_platform_private(device);
}

static void CFStringToWSTR(CFStringRef cstr, LPWSTR wstr, int length)
{
    int len = min(CFStringGetLength(cstr), length-1);
    CFStringGetCharacters(cstr, CFRangeMake(0, len), (UniChar*)wstr);
    wstr[len] = 0;
}

static DWORD CFNumberToDWORD(CFNumberRef num)
{
    int dwNum = 0;
    if (num)
        CFNumberGetValue(num, kCFNumberIntType, &dwNum);
    return dwNum;
}

static void handle_IOHIDDeviceIOHIDReportCallback(void *context,
        IOReturn result, void *sender, IOHIDReportType type,
        uint32_t reportID, uint8_t *report, CFIndex report_length)
{
    DEVICE_OBJECT *device = (DEVICE_OBJECT*)context;
    process_hid_report(device, report, report_length);
}

static int compare_platform_device(DEVICE_OBJECT *device, void *platform_dev)
{
    struct platform_private *private = impl_from_DEVICE_OBJECT(device);
    IOHIDDeviceRef dev2 = (IOHIDDeviceRef)platform_dev;
    if (private->device != dev2)
        return 1;
    else
        return 0;
}

static NTSTATUS get_reportdescriptor(DEVICE_OBJECT *device, BYTE *buffer, DWORD length, DWORD *out_length)
{
    struct platform_private *private = impl_from_DEVICE_OBJECT(device);
    CFDataRef data = IOHIDDeviceGetProperty(private->device, CFSTR(kIOHIDReportDescriptorKey));
    int data_length = CFDataGetLength(data);
    const UInt8 *ptr;

    *out_length = data_length;
    if (length < data_length)
        return STATUS_BUFFER_TOO_SMALL;

    ptr = CFDataGetBytePtr(data);
    memcpy(buffer, ptr, data_length);
    return STATUS_SUCCESS;
}

static NTSTATUS get_string(DEVICE_OBJECT *device, DWORD index, WCHAR *buffer, DWORD length)
{
    struct platform_private *private = impl_from_DEVICE_OBJECT(device);
    CFStringRef str;
    switch (index)
    {
        case HID_STRING_ID_IPRODUCT:
            str = IOHIDDeviceGetProperty(private->device, CFSTR(kIOHIDProductKey));
            break;
        case HID_STRING_ID_IMANUFACTURER:
            str = IOHIDDeviceGetProperty(private->device, CFSTR(kIOHIDManufacturerKey));
            break;
        case HID_STRING_ID_ISERIALNUMBER:
            str = IOHIDDeviceGetProperty(private->device, CFSTR(kIOHIDSerialNumberKey));
            break;
        default:
            ERR("Unknown string index\n");
            return STATUS_NOT_IMPLEMENTED;
    }

    if (str)
    {
        if (length < CFStringGetLength(str) + 1)
            return STATUS_BUFFER_TOO_SMALL;
        CFStringToWSTR(str, buffer, length);
    }
    else
    {
        if (!length) return STATUS_BUFFER_TOO_SMALL;
        buffer[0] = 0;
    }

    return STATUS_SUCCESS;
}

static NTSTATUS begin_report_processing(DEVICE_OBJECT *device)
{
    DWORD length;
    struct platform_private *private = impl_from_DEVICE_OBJECT(device);
    CFNumberRef num;

    if (private->buffer)
        return STATUS_SUCCESS;

    num = IOHIDDeviceGetProperty(private->device, CFSTR(kIOHIDMaxInputReportSizeKey));
    length = CFNumberToDWORD(num);
    private->buffer = HeapAlloc(GetProcessHeap(), 0, length);

    IOHIDDeviceRegisterInputReportCallback(private->device, private->buffer, length, handle_IOHIDDeviceIOHIDReportCallback, device);
    return STATUS_SUCCESS;
}

static NTSTATUS set_output_report(DEVICE_OBJECT *device, UCHAR id, BYTE *report, DWORD length, ULONG_PTR *written)
{
    IOReturn result;
    struct platform_private *private = impl_from_DEVICE_OBJECT(device);
    result = IOHIDDeviceSetReport(private->device, kIOHIDReportTypeOutput, id, report, length);
    if (result == kIOReturnSuccess)
    {
        *written = length;
        return STATUS_SUCCESS;
    }
    else
    {
        *written = 0;
        return STATUS_UNSUCCESSFUL;
    }
}

static NTSTATUS get_feature_report(DEVICE_OBJECT *device, UCHAR id, BYTE *report, DWORD length, ULONG_PTR *read)
{
    IOReturn ret;
    CFIndex report_length = length;
    struct platform_private *private = impl_from_DEVICE_OBJECT(device);

    ret = IOHIDDeviceGetReport(private->device, kIOHIDReportTypeFeature, id, report, &report_length);
    if (ret == kIOReturnSuccess)
    {
        *read = report_length;
        return STATUS_SUCCESS;
    }
    else
    {
        *read = 0;
        return STATUS_UNSUCCESSFUL;
    }
}

static NTSTATUS set_feature_report(DEVICE_OBJECT *device, UCHAR id, BYTE *report, DWORD length, ULONG_PTR *written)
{
    IOReturn result;
    struct platform_private *private = impl_from_DEVICE_OBJECT(device);

    result = IOHIDDeviceSetReport(private->device, kIOHIDReportTypeFeature, id, report, length);
    if (result == kIOReturnSuccess)
    {
        *written = length;
        return STATUS_SUCCESS;
    }
    else
    {
        *written = 0;
        return STATUS_UNSUCCESSFUL;
    }
}

static const platform_vtbl iohid_vtbl =
{
    compare_platform_device,
    get_reportdescriptor,
    get_string,
    begin_report_processing,
    set_output_report,
    get_feature_report,
    set_feature_report,
};

static void handle_DeviceMatchingCallback(void *context, IOReturn result, void *sender, IOHIDDeviceRef IOHIDDevice)
{
    DEVICE_OBJECT *device;
    DWORD vid, pid, version, uid;
    CFStringRef str = NULL;
    WCHAR serial_string[256];
    BOOL is_gamepad = FALSE;
    WORD input = -1;

    TRACE("OS/X IOHID Device Added %p\n", IOHIDDevice);

    vid = CFNumberToDWORD(IOHIDDeviceGetProperty(IOHIDDevice, CFSTR(kIOHIDVendorIDKey)));
    pid = CFNumberToDWORD(IOHIDDeviceGetProperty(IOHIDDevice, CFSTR(kIOHIDProductIDKey)));
    version = CFNumberToDWORD(IOHIDDeviceGetProperty(IOHIDDevice, CFSTR(kIOHIDVersionNumberKey)));
    str = IOHIDDeviceGetProperty(IOHIDDevice, CFSTR(kIOHIDSerialNumberKey));
    if (str) CFStringToWSTR(str, serial_string, ARRAY_SIZE(serial_string));
    uid = CFNumberToDWORD(IOHIDDeviceGetProperty(IOHIDDevice, CFSTR(kIOHIDLocationIDKey)));

    if (IOHIDDeviceConformsTo(IOHIDDevice, kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad) ||
       IOHIDDeviceConformsTo(IOHIDDevice, kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick))
    {
        if (is_xbox_gamepad(vid, pid))
            is_gamepad = TRUE;
        else
        {
            int axes=0, buttons=0;
            CFArrayRef element_array = IOHIDDeviceCopyMatchingElements(
                IOHIDDevice, NULL, kIOHIDOptionsTypeNone);

            if (element_array) {
                CFIndex index;
                CFIndex count = CFArrayGetCount(element_array);
                for (index = 0; index < count; index++)
                {
                    IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(element_array, index);
                    if (element)
                    {
                        int type = IOHIDElementGetType(element);
                        if (type == kIOHIDElementTypeInput_Button) buttons++;
                        if (type == kIOHIDElementTypeInput_Axis) axes++;
                        if (type == kIOHIDElementTypeInput_Misc)
                        {
                            uint32_t usage = IOHIDElementGetUsage(element);
                            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:
                                    axes ++;
                            }
                        }
                    }
                }
                CFRelease(element_array);
            }
            is_gamepad = (axes == 6  && buttons >= 14);
        }
    }
    if (is_gamepad)
        input = 0;

    device = bus_create_hid_device(busidW, vid, pid, input,
            version, uid, str ? serial_string : NULL, is_gamepad,
            &iohid_vtbl, sizeof(struct platform_private));
    if (!device)
        ERR("Failed to create device\n");
    else
    {
        struct platform_private *private = impl_from_DEVICE_OBJECT(device);
        private->device = IOHIDDevice;
        private->buffer = NULL;
        IoInvalidateDeviceRelations(bus_pdo, BusRelations);
    }
}

static void handle_RemovalCallback(void *context, IOReturn result, void *sender, IOHIDDeviceRef IOHIDDevice)
{
    DEVICE_OBJECT *device;
    TRACE("OS/X IOHID Device Removed %p\n", IOHIDDevice);
    IOHIDDeviceRegisterInputReportCallback(IOHIDDevice, NULL, 0, NULL, NULL);
    /* Note: Yes, we leak the buffer. But according to research there is no
             safe way to deallocate that buffer. */
    device = bus_find_hid_device(&iohid_vtbl, IOHIDDevice);
    if (device)
    {
        bus_unlink_hid_device(device);
        IoInvalidateDeviceRelations(bus_pdo, BusRelations);
        bus_remove_hid_device(device);
    }
}

/* This puts the relevant run loop for event handling into a WINE thread */
static DWORD CALLBACK runloop_thread(void *args)
{
    run_loop = CFRunLoopGetCurrent();

    IOHIDManagerSetDeviceMatching(hid_manager, NULL);
    IOHIDManagerRegisterDeviceMatchingCallback(hid_manager, handle_DeviceMatchingCallback, NULL);
    IOHIDManagerRegisterDeviceRemovalCallback(hid_manager, handle_RemovalCallback, NULL);
    IOHIDManagerScheduleWithRunLoop(hid_manager, run_loop, kCFRunLoopDefaultMode);
    if (IOHIDManagerOpen( hid_manager, 0 ) != kIOReturnSuccess)
    {
        ERR("Couldn't open IOHIDManager.\n");
        IOHIDManagerUnscheduleFromRunLoop(hid_manager, run_loop, kCFRunLoopDefaultMode);
        CFRelease(hid_manager);
        return 0;
    }

    CFRunLoopRun();
    TRACE("Run Loop exiting\n");
    return 1;

}

NTSTATUS iohid_driver_init(void)
{
    hid_manager = IOHIDManagerCreate(kCFAllocatorDefault, 0L);
    if (!(run_loop_handle = CreateThread(NULL, 0, runloop_thread, NULL, 0, NULL)))
    {
        ERR("Failed to initialize IOHID Manager thread\n");
        CFRelease(hid_manager);
        return STATUS_UNSUCCESSFUL;
    }

    TRACE("Initialization successful\n");
    return STATUS_SUCCESS;
}

void iohid_driver_unload( void )
{
    TRACE("Unloading Driver\n");

    if (!run_loop_handle)
        return;

    IOHIDManagerUnscheduleFromRunLoop(hid_manager, run_loop, kCFRunLoopDefaultMode);
    CFRunLoopStop(run_loop);
    WaitForSingleObject(run_loop_handle, INFINITE);
    CloseHandle(run_loop_handle);
    IOHIDManagerRegisterDeviceMatchingCallback(hid_manager, NULL, NULL);
    IOHIDManagerRegisterDeviceRemovalCallback(hid_manager, NULL, NULL);
    CFRelease(hid_manager);
    TRACE("Driver Unloaded\n");
}

#else

NTSTATUS iohid_driver_init(void)
{
    return STATUS_NOT_IMPLEMENTED;
}

void iohid_driver_unload( void )
{
    TRACE("Stub: Unload Driver\n");
}

#endif /* HAVE_IOHIDMANAGERCREATE */