/*
 * Copyright (C) 2009 Chase Douglas
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as published
 * by the Free Software Foundation.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL.  If you modify
 * file(s) with this exception, you may extend this exception to your
 * version of the file(s), but you are not obligated to do so.  If you
 * do not wish to do so, delete this exception statement from your
 * version.
 */

#include <QQueue>

#import <AppKit/AppKit.h>
#import <Carbon/Carbon.h>
#import <CoreAudio/CoreAudio.h>

#include "Connection.h"
#include "MacInputDevice.h"
#include "rinput.h"

static inline CGKeyCode translateUniversalKey(uint16_t code);
static inline CGKeyCode translateANSIKbKey(uint16_t code);

MacInputDevice::MacInputDevice(Connection *_connection) :
    InputDevice::InputDevice(_connection),
    created(FALSE),
    eventSource(NULL),
    dragLock(FALSE) {
    memset(&buttonState, 0, sizeof(buttonState));
    multiTapTimer.setSingleShot(TRUE);

    // It's ok if this fails, nothing terrible will happen
    eventSource = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);

    // This doesn't seem to do anything
    CGEventSourceSetPixelsPerLine(eventSource, 30);
}

// Nothing would cause a failure here
bool MacInputDevice::create() {
    return TRUE;
}

void MacInputDevice::handleMessage(rinput_message_t &message) {
    rinput_message_t errorMessage = { RINPUT_ERROR };

    switch (message.type) {
        case RINPUT_VERSION:
        case RINPUT_DESTROY:
            break;

        case RINPUT_SET_CAPABILITY:
            errorMessage.data.error.type = RINPUT_SET_CAPABILITY_FAILED;
            errorMessage.data.error.code1 = message.data.capability.type;
            errorMessage.data.error.code2 = message.data.capability.code;

            switch (message.data.capability.type) {
                case RINPUT_EV_KEY:
                    if (translateUniversalKey(message.data.capability.code) != 0xFF) {
                        break;
                    }
                    if (translateANSIKbKey(message.data.capability.code) != 0xFF) {
                        break;
                    }
                    if (message.data.capability.code == RINPUT_BTN_LEFT ||
                        message.data.capability.code == RINPUT_BTN_RIGHT ||
                        message.data.capability.code == RINPUT_BTN_MIDDLE ||
                        message.data.capability.code == RINPUT_KEY_MUTE ||
                        message.data.capability.code == RINPUT_KEY_VOLUMEUP ||
                        message.data.capability.code == RINPUT_KEY_VOLUMEDOWN) {
                        break;
                    }
                    qDebug("Warning: Remote key capability %hu unsupported", message.data.capability.code);
                    hton_rinput(&errorMessage);
                    connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
                    break;
                    
                case RINPUT_EV_REL:
                    if (message.data.capability.code == RINPUT_REL_X ||
                        message.data.capability.code == RINPUT_REL_Y ||
                        message.data.capability.code == RINPUT_REL_WHEEL ||
                        message.data.capability.code == RINPUT_REL_HWHEEL) {
                        break;
                    }
                    qDebug("Warning: Remote relative capability %hu unsupported", message.data.capability.code);
                    hton_rinput(&errorMessage);
                    connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
                    break;
                    
                case RINPUT_EV_UTF8:
                    break;
                    
                default:
                    qDebug("Warning: Remote capability type %hu unsupported", message.data.capability.type);
                    errorMessage.data.error.type = RINPUT_CAPABILITY_TYPE_INVALID;
                    hton_rinput(&errorMessage);
                    connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
                    break;
            }
            break;
            
        case RINPUT_SET_ABS_PARAM:
            qDebug("Warning: Remote absolute capabilities are unsupported");
            errorMessage.data.error.type = RINPUT_ABS_AXIS_INVALID;
            errorMessage.data.error.code1 = message.data.abs_param.axis;
            hton_rinput(&errorMessage);
            connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
            break;
            
        // Echo create message back to signify successful creation
        case RINPUT_CREATE:
            if (created) {
                qWarning("Warning: Input device create message received more than once");
                errorMessage.data.error.type = RINPUT_INPUT_DEVICE_CREATE_FAILED;
                hton_rinput(&errorMessage);
                connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
                break;
            }
            // Not really an error message, but the struct is already there for us to use
            errorMessage.type = RINPUT_CREATE;
            hton_rinput(&errorMessage);
            connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
            created = TRUE;
            break;

        case RINPUT_EVENT: {
            errorMessage.data.error.type = RINPUT_INPUT_EVENTS_FAILED;
            errorMessage.data.error.code1 = message.data.event.type;
            errorMessage.data.error.code2 = message.data.event.code;

            switch (message.data.event.type) {
                case RINPUT_EV_SYN:
                    break;
                    
                case RINPUT_EV_REL:
                    if (!handleRelativeMessage(message.data.event.code, message.data.event.value)) {
                        hton_rinput(&errorMessage);
                        connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
                        qWarning("Error: Remote relative code %hu invalid", message.data.event.code);
                    }
                    break;
                    
                case RINPUT_EV_KEY:
                    if (!handleKeyMessage(message.data.event.code, message.data.event.value)) {
                        hton_rinput(&errorMessage);
                        connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
                        qWarning("Error: Remote key code %hu invalid", message.data.event.code);
                    }
                    break;
                    
                case RINPUT_EV_UTF8:
                    if (!handleUTF8Message(message.data.utf8.character, message.data.utf8.value)) {
                        hton_rinput(&errorMessage);
                        connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
                        qWarning("Error: Remote UTF8 character (%hhX, %hhX, %hhX, %hhX) invalid", message.data.utf8.character[0], message.data.utf8.character[1], message.data.utf8.character[2], message.data.utf8.character[3]);
                    }
                    break;
                    
                default:
                    hton_rinput(&errorMessage);
                    connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
                    qWarning("Error: Remote event type %hu invalid", message.data.event.type);
            }
            break;
        }
            
        default:
            qWarning("Error: Remote message type invalid");
            errorMessage.data.error.type = RINPUT_MESSAGE_TYPE_INVALID;
            errorMessage.data.error.code1 = message.type;
            hton_rinput(&errorMessage);
            connection->write(QByteArray((const char *)&errorMessage, sizeof(errorMessage)));
            break;
    }
}

bool MacInputDevice::handleRelativeMessage(uint16_t code, int32_t value) {
    CGEventRef event;
    
    switch (code) {
        case RINPUT_REL_X: {
            CGPoint position = cursorPosition(value, 0);
            
            if (buttonState.leftButton) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventLeftMouseDragged, position, 0);
                dragLock = TRUE;
            }
            else if (buttonState.rightButton) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventRightMouseDragged, position, 0);
                dragLock = TRUE;
            }
            else if (buttonState.middleButton) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventOtherMouseDragged, position, kCGMouseButtonCenter);
                dragLock = TRUE;
            }
            else {
                event = CGEventCreateMouseEvent(eventSource, kCGEventMouseMoved, position, 0);
            }
            break;
        }
            
        case RINPUT_REL_Y: {
            CGPoint position = cursorPosition(0, value);
            
            if (buttonState.leftButton) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventLeftMouseDragged, position, 0);
                dragLock = TRUE;
            }
            else if (buttonState.rightButton) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventRightMouseDragged, position, 0);
                dragLock = TRUE;
            }
            else if (buttonState.middleButton) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventOtherMouseDragged, position, kCGMouseButtonCenter);
                dragLock = TRUE;
            }
            else {
                event = CGEventCreateMouseEvent(eventSource, kCGEventMouseMoved, position, 0);
            }
            break;
        }
            
        case RINPUT_REL_WHEEL: {
            event = CGEventCreateScrollWheelEvent(eventSource, kCGScrollEventUnitLine, 2, value, 0);
            break;
        }
            
        case RINPUT_REL_HWHEEL: {
            event = CGEventCreateScrollWheelEvent(eventSource, kCGScrollEventUnitLine, 2, 0, -value);
            break;
        }
        
        default:
            return FALSE;
    }
    
    CGEventPost(kCGHIDEventTap, event);
    CFRelease(event);
    
    return TRUE;
}

bool MacInputDevice::handleKeyMessage(uint16_t code, int32_t value) {
    QQueue<CGEventRef> events;
    CGEventRef event;
    
    if (dragLock && (code == RINPUT_BTN_LEFT || code == RINPUT_BTN_RIGHT || code == RINPUT_BTN_MIDDLE)) {
        if (value) {
            dragLock = FALSE;
            
            switch (code) {
                case RINPUT_BTN_LEFT:
                    buttonState.leftButton = (value && value != 256);
                    break;
                    
                case RINPUT_BTN_RIGHT:
                    buttonState.rightButton = (value && value != 256);
                    break;
                    
                case RINPUT_BTN_MIDDLE:
                    buttonState.middleButton = (value && value != 256);
                    break;
            }
        }
        else {
            return TRUE;
        }
    }
    
    if ((code == RINPUT_BTN_LEFT || code == RINPUT_BTN_RIGHT || code == RINPUT_BTN_MIDDLE) && value) {
        if (!multiTapTimer.isActive()) {
            multiTapTimer.start([NSEvent doubleClickInterval] * 1000);
            multiTapCount = 1;
        }
        else {
            multiTapCount++;
        }
    }

    switch (code) {
        case RINPUT_BTN_LEFT: {
            CGPoint position = cursorPosition(0, 0);
            if (value == 256) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventLeftMouseDown, position, 0);
                CGEventSetIntegerValueField(event, kCGMouseEventClickState, multiTapCount);
                events.enqueue(event);
                event = CGEventCreateMouseEvent(eventSource, kCGEventLeftMouseUp, position, 0);
                events.enqueue(event);
                buttonState.leftButton = FALSE;
            }
            else if (value) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventLeftMouseDown, position, 0);
                CGEventSetIntegerValueField(event, kCGMouseEventClickState, multiTapCount);
                events.enqueue(event);
                buttonState.leftButton = TRUE;
            }
            else {
                event = CGEventCreateMouseEvent(eventSource, kCGEventLeftMouseUp, position, 0);
                events.enqueue(event);
                buttonState.leftButton = FALSE;
            }
            break;
        }
            
        case RINPUT_BTN_RIGHT: {
            CGPoint position = cursorPosition(0, 0);
            if (value == 256) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventRightMouseDown, position, 0);
                CGEventSetIntegerValueField(event, kCGMouseEventClickState, multiTapCount);
                events.enqueue(event);
                event = CGEventCreateMouseEvent(eventSource, kCGEventRightMouseUp, position, 0);
                events.enqueue(event);
                buttonState.rightButton = FALSE;
            }
            else if (value) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventRightMouseDown, position, 0);
                CGEventSetIntegerValueField(event, kCGMouseEventClickState, multiTapCount);
                events.enqueue(event);
                buttonState.rightButton = TRUE;
            }
            else {
                event = CGEventCreateMouseEvent(eventSource, kCGEventRightMouseUp, position, 0);
                buttonState.rightButton = FALSE;
            }
            break;
        }
        
        case RINPUT_BTN_MIDDLE: {
            CGPoint position = cursorPosition(0, 0);
            if (value == 256) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventOtherMouseDown, position, kCGMouseButtonCenter);
                CGEventSetIntegerValueField(event, kCGMouseEventClickState, multiTapCount);
                events.enqueue(event);
                event = CGEventCreateMouseEvent(eventSource, kCGEventOtherMouseUp, position, kCGMouseButtonCenter);
                events.enqueue(event);
                buttonState.middleButton = FALSE;
            }
            else if (value) {
                event = CGEventCreateMouseEvent(eventSource, kCGEventOtherMouseDown, position, kCGMouseButtonCenter);
                CGEventSetIntegerValueField(event, kCGMouseEventClickState, multiTapCount);
                events.enqueue(event);
                buttonState.middleButton = TRUE;
            }
            else {
                event = CGEventCreateMouseEvent(eventSource, kCGEventOtherMouseUp, position, kCGMouseButtonCenter);
                events.enqueue(event);
                buttonState.middleButton = FALSE;
            }
            break;
        }
            
        default: {
            // WARNING: I have no clue how well all this will work if used on a machine with a non-ANSI keyboard!
            
            if (handleUniversalKeyMessage(code, value)) {
                break;
            }
            
            if (handleANSIKbKeyMessage(code, value)) {
                break;
            }
            
            if (handleMediaKeyMessage(code, value)) {
                break;
            }
            
            // Unrecognized key code
            return FALSE;
        }
    }
    
    while (!events.isEmpty()) {
        event = events.dequeue();
        CGEventPost(kCGHIDEventTap, event);
        CFRelease(event);
    }
    
    return TRUE;
}

#define TRANSLATE(a, b) case RINPUT_KEY_##a: \
return kVK_##b;
static inline CGKeyCode translateUniversalKey(uint16_t code) {
    switch (code) {
            TRANSLATE(ENTER, Return);
            TRANSLATE(TAB, Tab);
            TRANSLATE(SPACE, Space);
            TRANSLATE(BACKSPACE, Delete);
            TRANSLATE(ESC, Escape);
            TRANSLATE(LEFTMETA, Command);
            TRANSLATE(RIGHTMETA, Command);
            TRANSLATE(LEFTSHIFT, Shift);
            TRANSLATE(CAPSLOCK, CapsLock);
            TRANSLATE(LEFTALT, Option);
            TRANSLATE(OPTION, Option);
            TRANSLATE(LEFTCTRL, Control);
            TRANSLATE(RIGHTSHIFT, RightShift);
            TRANSLATE(RIGHTALT, RightOption);
            TRANSLATE(RIGHTCTRL, RightControl);
            TRANSLATE(FN, Function);
            TRANSLATE(F17, F17);
            TRANSLATE(F18, F18);
            TRANSLATE(F19, F19);
            TRANSLATE(F20, F20);
            TRANSLATE(F5, F5);
            TRANSLATE(F6, F6);
            TRANSLATE(F7, F7);
            TRANSLATE(F3, F3);
            TRANSLATE(F8, F8);
            TRANSLATE(F9, F9);
            TRANSLATE(F11, F11);
            TRANSLATE(F13, F13);
            TRANSLATE(F16, F16);
            TRANSLATE(F14, F14);
            TRANSLATE(F10, F10);
            TRANSLATE(F12, F12);
            TRANSLATE(F15, F15);
            TRANSLATE(HELP, Help);
            TRANSLATE(HOME, Home);
            TRANSLATE(PAGEUP, PageUp);
            TRANSLATE(DELETE, ForwardDelete);
            TRANSLATE(F4, F4);
            TRANSLATE(END, End);
            TRANSLATE(F2, F2);
            TRANSLATE(PAGEDOWN, PageDown);
            TRANSLATE(F1, F1);
            TRANSLATE(LEFT, LeftArrow);
            TRANSLATE(RIGHT, RightArrow);
            TRANSLATE(DOWN, DownArrow);
            TRANSLATE(UP, UpArrow);
            
        default:
            return 0xff; // Invalid key code
    }
}
#undef TRANSLATE

bool MacInputDevice::handleUniversalKeyMessage(uint16_t code, int32_t value) {
    CGKeyCode key = translateUniversalKey(code);
    if (key == 0xFF) {
        return FALSE;
    }
    
    CGEventRef event = CGEventCreateKeyboardEvent(eventSource, key, value);
    CGEventPost(kCGHIDEventTap, event);
    CFRelease(event);
    if (value == 256) {
        event = CGEventCreateKeyboardEvent(eventSource, key, FALSE);
        CGEventPost(kCGHIDEventTap, event);
        CFRelease(event);
    }
    
    return TRUE;
}

#define TRANSLATE(a, b) if (code == RINPUT_KEY_##a) return kVK_ANSI_##b
#define TRANSLATE_ALPHANUM(a) if (code == RINPUT_KEY_##a) return kVK_ANSI_##a
static inline CGKeyCode translateANSIKbKey(uint16_t code) {
    TRANSLATE(GRAVE, Grave);
    TRANSLATE_ALPHANUM(1);
    TRANSLATE_ALPHANUM(2);
    TRANSLATE_ALPHANUM(3);
    TRANSLATE_ALPHANUM(4);
    TRANSLATE_ALPHANUM(5);
    TRANSLATE_ALPHANUM(6);
    TRANSLATE_ALPHANUM(7);
    TRANSLATE_ALPHANUM(8);
    TRANSLATE_ALPHANUM(9);
    TRANSLATE_ALPHANUM(0);
    TRANSLATE(MINUS, Minus);
    TRANSLATE(EQUAL, Equal);
    TRANSLATE_ALPHANUM(Q);
    TRANSLATE_ALPHANUM(W);
    TRANSLATE_ALPHANUM(E);
    TRANSLATE_ALPHANUM(R);
    TRANSLATE_ALPHANUM(T);
    TRANSLATE_ALPHANUM(Y);
    TRANSLATE_ALPHANUM(U);
    TRANSLATE_ALPHANUM(I);
    TRANSLATE_ALPHANUM(O);
    TRANSLATE_ALPHANUM(P);
    TRANSLATE(LEFTBRACE, LeftBracket);
    TRANSLATE(RIGHTBRACE, RightBracket);
    TRANSLATE(BACKSLASH, Backslash);
    TRANSLATE_ALPHANUM(A);
    TRANSLATE_ALPHANUM(S);
    TRANSLATE_ALPHANUM(D);
    TRANSLATE_ALPHANUM(F);
    TRANSLATE_ALPHANUM(G);
    TRANSLATE_ALPHANUM(H);
    TRANSLATE_ALPHANUM(J);
    TRANSLATE_ALPHANUM(K);
    TRANSLATE_ALPHANUM(L);
    TRANSLATE(SEMICOLON, Semicolon);
    TRANSLATE(APOSTROPHE, Quote);
    TRANSLATE_ALPHANUM(Z);
    TRANSLATE_ALPHANUM(X);
    TRANSLATE_ALPHANUM(C);
    TRANSLATE_ALPHANUM(V);
    TRANSLATE_ALPHANUM(B);
    TRANSLATE_ALPHANUM(N);
    TRANSLATE_ALPHANUM(M);
    TRANSLATE(KPASTERISK, KeypadMultiply);
    TRANSLATE(COMMA, Comma);
    TRANSLATE(DOT, Period);
    TRANSLATE(SLASH, Slash);
    TRANSLATE(KP7, Keypad7);
    TRANSLATE(KP8, Keypad7);
    TRANSLATE(KP9, Keypad7);
    TRANSLATE(KPMINUS, KeypadMinus);
    TRANSLATE(KP4, Keypad7);
    TRANSLATE(KP5, Keypad7);
    TRANSLATE(KP6, Keypad7);
    TRANSLATE(KPPLUS, KeypadPlus);
    TRANSLATE(KP1, Keypad7);
    TRANSLATE(KP2, Keypad7);
    TRANSLATE(KP3, Keypad7);
    TRANSLATE(KP0, Keypad7);
    TRANSLATE(KPDOT, KeypadDecimal);
    TRANSLATE(KPENTER, KeypadEnter);
    TRANSLATE(KPSLASH, KeypadDivide);
    TRANSLATE(KPEQUAL, KeypadEquals);
    TRANSLATE(KPPLUSMINUS, KeypadPlus);
    
    return 0xFF; // Invalid key code
}
#undef TRANSLATE
#undef TRANSLATE_ALPHANUM

bool MacInputDevice::handleANSIKbKeyMessage(uint16_t code, int32_t value) {
    CGKeyCode key = translateANSIKbKey(code);
    if (key == 0xFF) {
        return FALSE;
    }
    
    CGEventRef event = CGEventCreateKeyboardEvent(eventSource, key, value);
    CGEventPost(kCGHIDEventTap, event);
    CFRelease(event);
    if (value == 256) {
        CGEventRef event = CGEventCreateKeyboardEvent(eventSource, key, FALSE);
        CGEventPost(kCGHIDEventTap, event);
        CFRelease(event);
    }
    
    return TRUE;
}

static inline Float32 systemVolume(AudioDeviceID device) {
    Float32 volume[2];
    UInt32 size = sizeof(Float32);
    AudioObjectPropertyAddress address = {
        kAudioDevicePropertyVolumeScalar,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMaster
    };
    
    if (AudioObjectGetPropertyData(device, &address, 0, NULL, &size, volume) == noErr) {
        return volume[0];
    }
    
    address.mSelector = kAudioDevicePropertyPreferredChannelsForStereo;
    UInt32 channels[2];
    size = sizeof(channels);
    if (AudioObjectGetPropertyData(device, &address, 0, NULL, &size, channels) != noErr) {
        qWarning("Warning: Failed to get master volume or get stereo channels");
        return -1;
    }
    
    address.mSelector = kAudioDevicePropertyVolumeScalar;
    address.mElement = channels[0];
    size = sizeof(Float32);
    if (AudioObjectGetPropertyData(device, &address, 0, NULL, &size, volume) != noErr) {
        qWarning("Warning: Failed to get master or stereo volumes");
        return -1;
    }
    
    address.mElement = channels[1];
    size = sizeof(Float32);
    if (AudioObjectGetPropertyData(device, &address, 0, NULL, &size, volume + 1) != noErr) {
        qWarning("Warning: Failed to get master or stereo volumes");
        return -1;
    }

    return (volume[0] + volume[1]) / 2;
}

static inline void setSystemVolume(AudioDeviceID device, Float32 volume) {
    UInt32 size = sizeof(volume);
    AudioObjectPropertyAddress address = {
        kAudioDevicePropertyVolumeScalar,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMaster
    };

    if (AudioObjectSetPropertyData(device, &address, 0, NULL, size, &volume) == noErr) {
        return;
    }
    
    address.mSelector = kAudioDevicePropertyPreferredChannelsForStereo;
    UInt32 channels[2];
    size = sizeof(channels);
    if (AudioObjectGetPropertyData(device, &address, 0, NULL, &size, channels) != noErr) {
        qWarning("Warning: Failed to set master volume or get stereo channels");
        return;
    }
    
    address.mSelector = kAudioDevicePropertyVolumeScalar;
    address.mElement = channels[0];
    size = sizeof(volume);
    if (AudioObjectSetPropertyData(device, &address, 0, NULL, size, &volume) != noErr) {
        qWarning("Warning: Failed to set master or stereo volumes");
        return;
    }
    
    address.mElement = channels[1];
    size = sizeof(volume);
    if (AudioObjectSetPropertyData(device, &address, 0, NULL, size, &volume) != noErr) {
        qWarning("Warning: Failed to set master or stereo volumes");
        return;
    }
}

bool MacInputDevice::handleMediaKeyMessage(uint16_t code, int32_t value) {
    if (code == RINPUT_KEY_MUTE || code == RINPUT_KEY_VOLUMEDOWN || code == RINPUT_KEY_VOLUMEUP) {
        AudioDeviceID device = kAudioObjectUnknown;
        UInt32 size = sizeof(device);
        AudioObjectPropertyAddress address = {
            kAudioHardwarePropertyDefaultOutputDevice,
            kAudioObjectPropertyScopeGlobal,
            kAudioObjectPropertyElementMaster
        };
        if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, &size, &device) != noErr || device == kAudioObjectUnknown) {
            qWarning("Warning: Failed to get default audio output device");
            return TRUE;
        }
        
        if (code == RINPUT_KEY_MUTE) {
            UInt32 mute;
            size = sizeof(mute);
            address.mSelector = kAudioDevicePropertyMute;
            address.mScope = kAudioDevicePropertyScopeOutput;
            if (AudioObjectGetPropertyData(device, &address, 0, NULL, &size, &mute) != noErr) {
                qWarning("Warning: Failed to get audio output master mute value");
                return TRUE;
            }
            
            mute = !mute;
            size = sizeof(mute);
            if (AudioObjectSetPropertyData(device, &address, 0, NULL, size, &mute) != noErr) {
                qWarning("Warning: Failed to set audio output master mute value");
                return TRUE;
            }
        }
        else if (code == RINPUT_KEY_VOLUMEDOWN || code == RINPUT_KEY_VOLUMEUP) {
            Float32 volume = systemVolume(device);
            if (volume < 0 || volume > 1) {
                return TRUE;
            }
            
            volume += (Float32)(code == RINPUT_KEY_VOLUMEUP ? 1 : -1) / 16;
            if (volume > 1) {
                volume = 1;
            }
            else if (volume < 0) {
                volume = 0;
            }
            
            setSystemVolume(device, volume);
        }
        
        return TRUE;
    }
    return FALSE;
}

bool MacInputDevice::handleUTF8Message(unsigned char character[4], int16_t value) {
    NSUInteger length = 1;
    if (character[0] >= 0xC2 && character[0] <= 0xDF) {
        length = 2;
    }
    else if (character[0] >= 0xE0 && character[0] <= 0xEF) {
        length = 3;
    }
    else if (character[0] >= 0xF0 && character[0] <= 0xF4) {
        length = 4;
    }
    
    NSString *unicodeString = [[NSString alloc] initWithBytesNoCopy:character length:length encoding:NSUTF8StringEncoding freeWhenDone:NO];
    if (unicodeString.length != 1) {
        [unicodeString release];
        return FALSE;
    }
    
    unichar unicode[2];
    [unicodeString getCharacters:unicode range:NSMakeRange(0, 1)];
    [unicodeString release];

    CGEventRef event = CGEventCreateKeyboardEvent(eventSource, kVK_ISO_Section, value);
    CGEventKeyboardSetUnicodeString(event, 1, unicode);
    CGEventPost(kCGHIDEventTap, event);
    CFRelease(event);
    if (value == 256) {
        CGEventRef event = CGEventCreateKeyboardEvent(eventSource, kVK_ISO_Section, FALSE);
        CGEventKeyboardSetUnicodeString(event, 1, unicode);
        CGEventPost(kCGHIDEventTap, event);
        CFRelease(event);
    }
    
    return TRUE;
}

// Fuzzy logic needed for float comparisons
#define EPSILON 0.1
static inline BOOL CGRectContainsPointFuzzy(CGRect rect, CGPoint point) {
    if (point.x > rect.origin.x - EPSILON &&
        point.x < rect.origin.x + rect.size.width + EPSILON &&
        point.y > rect.origin.y - EPSILON &&
        point.y < rect.origin.y + rect.size.height + EPSILON) {
        return YES;
    }
    else {
        return NO;
    }
}

enum Direction {
    UP,
    DOWN,
    LEFT,
    RIGHT
};

// Only one of the arguments will be inequal to 0
CGPoint MacInputDevice::cursorPosition(CGFloat dx, CGFloat dy) {
    CGPoint position = NSPointToCGPoint([NSEvent mouseLocation]);
    NSArray *screens = [NSScreen screens];
    NSScreen *screen;
    
    Direction direction = RIGHT;
    if (dx < 0) direction = LEFT;
    else if (dy > 0) direction = UP;
    else if (dy < 0) direction = DOWN;
    
    CGFloat maxScreenHeight = 0;
    for (screen in screens) {
        if (screen.frame.size.height > maxScreenHeight) {
            maxScreenHeight = screen.frame.size.height;
        }
    }
    
    // Flip y-axis
    position.y = maxScreenHeight - position.y;

    // Keep moving to the edge of the screen the position is in until we reach the end of the screens or the desired position
    forever {
        CGRect frame;
        
        for (screen in screens) {
            frame = NSRectToCGRect(screen.frame);
            frame.origin.y = maxScreenHeight - frame.size.height;
            frame.size.width--;
            frame.size.height--;
            
            if (CGRectContainsPointFuzzy(frame, position)) {
                break;
            }
        }
        
        // We've reached the end of a screen and there aren't any more screens next to it
        if (!screen) {
            // Backtrack so we aren't off screen
            if (direction == RIGHT) position.x--;
            else if (direction == LEFT) position.x++;
            else if (direction == UP) position.y--;
            else if (direction == DOWN) position.y++;
            return position;
        }
        
        // If our intended point is in the screen, go to it
        if (CGRectContainsPointFuzzy(frame, CGPointMake(position.x + dx, position.y + dy))) {
            position.x += dx;
            position.y += dy;
            return position;
        }
        
        // Go to a pixel past the edge of this screen
        // We rely on the fact that only one of dx or dy is inequal to 0
        if (dx > 0) {
            dx -= CGRectGetMaxX(frame) + 1 - position.x;
            position.x = CGRectGetMaxX(frame) + 1;
        }
        else if (dx < 0) {
            dx -= CGRectGetMinX(frame) - 1 - position.x;
            position.x = CGRectGetMinX(frame) - 1;
        }
        else if (dy > 0) {
            dy -= CGRectGetMaxY(frame) + 1 - position.y;
            position.y = CGRectGetMaxY(frame) + 1;
        }
        else if (dy < 0) {
            dy -= CGRectGetMinY(frame) - 1 - position.y;
            position.y = CGRectGetMinY(frame) - 1;
        }
    }
}

MacInputDevice::~MacInputDevice() {
    if (eventSource) {
        CFRelease(eventSource);
    }
}
