#import <IOKit/pwr_mgt/IOPMLib.h>
#import <Carbon/Carbon.h>
#import "GBView.h"
#import "GBViewGL.h"
#import "GBViewMetal.h"
#import "GBButtons.h"
#import "NSString+StringForKey.h"
#import "NSObject+DefaultsObserver.h"
#import "Document.h"

#define JOYSTICK_HIGH 0x4000
#define JOYSTICK_LOW 0x3800

static const uint8_t workboy_ascii_to_key[] = {
    ['0'] = GB_WORKBOY_0,
    ['`'] = GB_WORKBOY_UMLAUT,
    ['1'] = GB_WORKBOY_1,
    ['2'] = GB_WORKBOY_2,
    ['3'] = GB_WORKBOY_3,
    ['4'] = GB_WORKBOY_4,
    ['5'] = GB_WORKBOY_5,
    ['6'] = GB_WORKBOY_6,
    ['7'] = GB_WORKBOY_7,
    ['8'] = GB_WORKBOY_8,
    ['9'] = GB_WORKBOY_9,
    
    ['\r'] = GB_WORKBOY_ENTER,
    [3] = GB_WORKBOY_ENTER,
    
    ['!'] = GB_WORKBOY_EXCLAMATION_MARK,
    ['$'] = GB_WORKBOY_DOLLAR,
    ['#'] = GB_WORKBOY_HASH,
    ['~'] = GB_WORKBOY_TILDE,
    ['*'] = GB_WORKBOY_ASTERISK,
    ['+'] = GB_WORKBOY_PLUS,
    ['-'] = GB_WORKBOY_MINUS,
    ['('] = GB_WORKBOY_LEFT_PARENTHESIS,
    [')'] = GB_WORKBOY_RIGHT_PARENTHESIS,
    [';'] = GB_WORKBOY_SEMICOLON,
    [':'] = GB_WORKBOY_COLON,
    ['%'] = GB_WORKBOY_PERCENT,
    ['='] = GB_WORKBOY_EQUAL,
    [','] = GB_WORKBOY_COMMA,
    ['<'] = GB_WORKBOY_LT,
    ['.'] = GB_WORKBOY_DOT,
    ['>'] = GB_WORKBOY_GT,
    ['/'] = GB_WORKBOY_SLASH,
    ['?'] = GB_WORKBOY_QUESTION_MARK,
    [' '] = GB_WORKBOY_SPACE,
    ['\''] = GB_WORKBOY_QUOTE,
    ['@'] = GB_WORKBOY_AT,
    
    ['q'] = GB_WORKBOY_Q,
    ['w'] = GB_WORKBOY_W,
    ['e'] = GB_WORKBOY_E,
    ['r'] = GB_WORKBOY_R,
    ['t'] = GB_WORKBOY_T,
    ['y'] = GB_WORKBOY_Y,
    ['u'] = GB_WORKBOY_U,
    ['i'] = GB_WORKBOY_I,
    ['o'] = GB_WORKBOY_O,
    ['p'] = GB_WORKBOY_P,
    ['a'] = GB_WORKBOY_A,
    ['s'] = GB_WORKBOY_S,
    ['d'] = GB_WORKBOY_D,
    ['f'] = GB_WORKBOY_F,
    ['g'] = GB_WORKBOY_G,
    ['h'] = GB_WORKBOY_H,
    ['j'] = GB_WORKBOY_J,
    ['k'] = GB_WORKBOY_K,
    ['l'] = GB_WORKBOY_L,
    ['z'] = GB_WORKBOY_Z,
    ['x'] = GB_WORKBOY_X,
    ['c'] = GB_WORKBOY_C,
    ['v'] = GB_WORKBOY_V,
    ['b'] = GB_WORKBOY_B,
    ['n'] = GB_WORKBOY_N,
    ['m'] = GB_WORKBOY_M,
};

static const uint8_t workboy_vk_to_key[] = {
    [kVK_F1] = GB_WORKBOY_CLOCK,
    [kVK_F2] = GB_WORKBOY_TEMPERATURE,
    [kVK_F3] = GB_WORKBOY_MONEY,
    [kVK_F4] = GB_WORKBOY_CALCULATOR,
    [kVK_F5] = GB_WORKBOY_DATE,
    [kVK_F6] = GB_WORKBOY_CONVERSION,
    [kVK_F7] = GB_WORKBOY_RECORD,
    [kVK_F8] = GB_WORKBOY_WORLD,
    [kVK_F9] = GB_WORKBOY_PHONE,
    [kVK_F10] = GB_WORKBOY_UNKNOWN,
    [kVK_Delete] = GB_WORKBOY_BACKSPACE,
    [kVK_Shift] = GB_WORKBOY_SHIFT_DOWN,
    [kVK_RightShift] = GB_WORKBOY_SHIFT_DOWN,
    [kVK_UpArrow] = GB_WORKBOY_UP,
    [kVK_DownArrow] = GB_WORKBOY_DOWN,
    [kVK_LeftArrow] = GB_WORKBOY_LEFT,
    [kVK_RightArrow] = GB_WORKBOY_RIGHT,
    [kVK_Escape] = GB_WORKBOY_ESCAPE,
    [kVK_ANSI_KeypadDecimal] = GB_WORKBOY_DECIMAL_POINT,
    [kVK_ANSI_KeypadClear] = GB_WORKBOY_M,
    [kVK_ANSI_KeypadMultiply] = GB_WORKBOY_H,
    [kVK_ANSI_KeypadDivide] = GB_WORKBOY_J,
};

@implementation GBView
{
    bool mouse_hidden;
    NSTrackingArea *tracking_area;
    bool _mouseHidingEnabled;
    bool axisActive[2];
    bool underclockKeyDown;
    double clockMultiplier;
    double analogClockMultiplier;
    bool analogClockMultiplierValid;
    NSEventModifierFlags previousModifiers;
    JOYController *lastController;
    bool _turbo;
    bool _mouseControlEnabled;
    NSMutableDictionary<NSNumber *, JOYController *> *_controllerMapping;
    unsigned _lastPlayerCount;
    
    bool _rapidA[4], _rapidB[4];
    uint8_t _rapidACount[4], _rapidBCount[4];
}

+ (instancetype)alloc
{
    return [self allocWithZone:NULL];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (self == [GBView class]) {
        if ([GBViewMetal isSupported]) {
            return [GBViewMetal allocWithZone: zone];
        }
        return [GBViewGL allocWithZone: zone];
    }
    return [super allocWithZone:zone];
}

- (void) _init
{
    [self registerForDraggedTypes:[NSArray arrayWithObjects: NSFilenamesPboardType, nil]];
    
    __unsafe_unretained GBView *weakSelf = self;
    [self observeStandardDefaultsKey:@"GBAspectRatioUnkept" withBlock:^(id newValue) {
        [weakSelf setFrame:weakSelf.superview.frame];
    }];
    [self observeStandardDefaultsKey:@"GBForceIntegerScale" withBlock:^(id newValue) {
        [weakSelf setFrame:weakSelf.superview.frame];
    }];
    [self observeStandardDefaultsKey:@"JoyKitDefaultControllers" withBlock:^(id newValue) {
        [weakSelf reassignControllers];
    }];
    tracking_area = [ [NSTrackingArea alloc] initWithRect:(NSRect){}
                                                  options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseMoved
                                                    owner:self
                                                 userInfo:nil];
    [self addTrackingArea:tracking_area];
    clockMultiplier = 1.0;
    [self createInternalView];
    [self addSubview:self.internalView];
    self.internalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
    [JOYController registerListener:self];
    _mouseControlEnabled = true;
    [self reassignControllers];
}

- (void)controllerConnected:(JOYController *)controller
{
    [self reassignControllers];
}

- (void)controllerDisconnected:(JOYController *)controller
{
    [self reassignControllers];
}

- (unsigned)playerCount
{
    if (self.document.partner) {
        return 2;
    }
    if (!_gb) {
        return 1;
    }
    return GB_get_player_count(_gb);
}

- (void)reassignControllers
{
    unsigned playerCount = self.playerCount;
    /* Don't assign controlelrs if there's only one player, allow all controllers. */
    if (playerCount == 1) {
        _controllerMapping = [NSMutableDictionary dictionary];
        return;
    }
    
    if (!_controllerMapping) {
        _controllerMapping = [NSMutableDictionary dictionary];
    }
    
    for (NSNumber *player in [_controllerMapping copy]) {
        if (player.unsignedIntValue >= playerCount || !_controllerMapping[player].connected) {
            [_controllerMapping removeObjectForKey:player];
        }
    }
    
    _lastPlayerCount = playerCount;
    for (unsigned i = 0; i < playerCount; i++) {
        NSString *preferredJoypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"]
                                      objectForKey:n2s(i)];
        for (JOYController *controller in [JOYController allControllers]) {
            if (!controller.connected) continue;
            if ([controller.uniqueID isEqual:preferredJoypad]) {
                _controllerMapping[@(i)] = controller;
                break;
            }
        }
    }
}

- (void)tryAssigningController:(JOYController *)controller
{
    unsigned playerCount = self.playerCount;
    if (playerCount == 1) return;
    if (_controllerMapping.count == playerCount) return;
    if ([_controllerMapping.allValues containsObject:controller]) return;
    for (unsigned i = 0; i < playerCount; i++) {
        if (!_controllerMapping[@(i)]) {
            _controllerMapping[@(i)] = controller;
            return;
        }
    }
}

- (NSDictionary<NSNumber *, JOYController *> *)controllerMapping
{
    if (_lastPlayerCount != self.playerCount) {
        [self reassignControllers];
    }
    
    return _controllerMapping;
}

- (void)screenSizeChanged
{
    [super screenSizeChanged];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self setFrame:self.superview.frame];
    });
}

- (void)dealloc
{
    if (mouse_hidden) {
        mouse_hidden = false;
        [NSCursor unhide];
    }
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self setRumble:0];
    [JOYController unregisterListener:self];
}

- (instancetype)initWithCoder:(NSCoder *)coder
{
    if (!(self = [super initWithCoder:coder])) { 
        return self;
    }
    [self _init];
    return self;
}

- (instancetype)initWithFrame:(NSRect)frameRect
{
    if (!(self = [super initWithFrame:frameRect])) { 
        return self;
    }
    [self _init];
    return self;
}

- (void)setFrame:(NSRect)frame
{
    NSView *superview = self.superview;
    if (GB_unlikely(!superview)) return;
    frame = superview.frame;
    if (_gb && ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAspectRatioUnkept"]) {
        double ratio = frame.size.width / frame.size.height;
        double width = GB_get_screen_width(_gb);
        double height = GB_get_screen_height(_gb);
        if (ratio >= width / height) {
            double new_width = round(frame.size.height / height * width);
            frame.origin.x = floor((frame.size.width - new_width) / 2);
            frame.size.width = new_width;
            frame.origin.y = 0;
        }
        else {
            double new_height = round(frame.size.width / width * height);
            frame.origin.y = floor((frame.size.height - new_height) / 2);
            frame.size.height = new_height;
            frame.origin.x = 0;
        }
    }
    
    if (_gb && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBForceIntegerScale"]) {
        double factor = self.window.backingScaleFactor;
        double width = GB_get_screen_width(_gb) / factor;
        double height = GB_get_screen_height(_gb) / factor;
        
        double new_width = floor(frame.size.width / width)  * width;
        double new_height = floor(frame.size.height / height)  * height;
        frame.origin.x += floor((frame.size.width - new_width) / 2);
        frame.origin.y += floor((frame.size.height - new_height) / 2);
        frame.size.width = new_width;
        frame.size.height = new_height;
    }

    [super setFrame:frame];
}

- (void) flip
{
    if (analogClockMultiplierValid && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]) {
        clockMultiplier = 1.0;
        GB_set_clock_multiplier(_gb, analogClockMultiplier);
        if (self.document.partner) {
            GB_set_clock_multiplier(self.document.partner.gb, analogClockMultiplier);
        }
        if (analogClockMultiplier == 1.0) {
            analogClockMultiplierValid = false;
        }
        if (analogClockMultiplier < 2.0 && analogClockMultiplier > 1.0) {
            GB_set_turbo_mode(_gb, false, false);
            if (self.document.partner) {
                GB_set_turbo_mode(self.document.partner.gb, false, false);
            }
        }
    }
    else {
        if (underclockKeyDown && clockMultiplier > 0.5) {
            clockMultiplier -= 1.0/16;
            GB_set_clock_multiplier(_gb, clockMultiplier);
            if (self.document.partner) {
                GB_set_clock_multiplier(self.document.partner.gb, clockMultiplier);
            }
        }
        if (!underclockKeyDown && clockMultiplier < 1.0) {
            clockMultiplier += 1.0/16;
            GB_set_clock_multiplier(_gb, clockMultiplier);
            if (self.document.partner) {
                GB_set_clock_multiplier(self.document.partner.gb, clockMultiplier);
            }
        }
    }
    if ((!analogClockMultiplierValid && clockMultiplier > 1) ||
        _turbo || (analogClockMultiplierValid && analogClockMultiplier > 1)) {
        [self.osdView displayText:@"Fast forwarding…"];
    }
    else if ((!analogClockMultiplierValid && clockMultiplier < 1) ||
             (analogClockMultiplierValid && analogClockMultiplier < 1)) {
        [self.osdView displayText:@"Slow motion…"];
    }
    
    for (unsigned i = GB_get_player_count(_gb); i--;) {
        if (_rapidA[i]) {
            _rapidACount[i]++;
            GB_set_key_state_for_player(_gb, GB_KEY_A, i, !(_rapidACount[i] & 2));
        }
        if (_rapidB[i]) {
            _rapidBCount[i]++;
            GB_set_key_state_for_player(_gb, GB_KEY_B, i, !(_rapidBCount[i] & 2));
        }
    }
    [super flip];
}

-(void)keyDown:(NSEvent *)theEvent
{
    if ([theEvent type] != NSEventTypeFlagsChanged && theEvent.isARepeat) return;
    unsigned short keyCode = theEvent.keyCode;
    if (GB_workboy_is_enabled(_gb)) {
        if (theEvent.keyCode < sizeof(workboy_vk_to_key) && workboy_vk_to_key[theEvent.keyCode]) {
            GB_workboy_set_key(_gb, workboy_vk_to_key[theEvent.keyCode]);
            return;
        }
        unichar c = [theEvent type] != NSEventTypeFlagsChanged? [theEvent.charactersIgnoringModifiers.lowercaseString characterAtIndex:0] : 0;
        if (c < sizeof(workboy_ascii_to_key) && workboy_ascii_to_key[c]) {
            GB_workboy_set_key(_gb, workboy_ascii_to_key[c]);
            return;
        }
    }
    
    bool handled = false;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    unsigned player_count = GB_get_player_count(_gb);
    if (self.document.partner) {
        player_count = 2;
    }
    for (unsigned player = 0; player < player_count; player++) {
        for (GBButton button = 0; button < GBKeyboardButtonCount; button++) {
            NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)];
            if (!key) continue;

            if (key.unsignedShortValue == keyCode) {
                handled = true;
                switch (button) {
                    case GBTurbo:
                        if (self.document.isSlave) {
                            GB_set_turbo_mode(self.document.partner.gb, true, false);
                        }
                        else {
                            GB_set_turbo_mode(_gb, true, self.isRewinding);
                        }
                        _turbo = true;
                        analogClockMultiplierValid = false;
                        break;
                        
                    case GBRewind:
                        if (!self.document.partner) {
                            self.isRewinding = true;
                            GB_set_turbo_mode(_gb, false, false);
                            _turbo = false;
                        }
                        break;
                        
                    case GBUnderclock:
                        underclockKeyDown = true;
                        analogClockMultiplierValid = false;
                        break;
                        
                    case GBRapidA:
                        _rapidA[player] = true;
                        _rapidACount[player] = 0;
                        GB_set_key_state_for_player(_gb, GB_KEY_A, player, true);
                        break;
                        
                    case GBRapidB:
                        _rapidB[player] = true;
                        _rapidBCount[player] = 0;
                        GB_set_key_state_for_player(_gb, GB_KEY_B, player, true);
                        break;
                        
                    default:
                        if (self.document.partner) {
                            if (player == 0) {
                                GB_set_key_state_for_player(_gb, (GB_key_t)button, 0, true);
                                if ((GB_key_t)button <= GB_KEY_DOWN) {
                                    GB_set_use_faux_analog_inputs(_gb, 0, false);
                                }
                            }
                            else {
                                GB_set_key_state_for_player(self.document.partner.gb, (GB_key_t)button, 0, true);
                                if ((GB_key_t)button <= GB_KEY_DOWN) {
                                    GB_set_use_faux_analog_inputs(self.document.partner.gb, 0, false);
                                }
                            }
                        }
                        else {
                            GB_set_key_state_for_player(_gb, (GB_key_t)button, player, true);
                            if ((GB_key_t)button <= GB_KEY_DOWN) {
                                GB_set_use_faux_analog_inputs(_gb, player, false);
                            }
                        }
                        break;
                }
            }
        }
    }

    if (!handled && [theEvent type] != NSEventTypeFlagsChanged) {
        [super keyDown:theEvent];
    }
}

-(void)keyUp:(NSEvent *)theEvent
{
    unsigned short keyCode = theEvent.keyCode;
    if (GB_workboy_is_enabled(_gb)) {
        if (keyCode == kVK_Shift || keyCode == kVK_RightShift) {
            GB_workboy_set_key(_gb, GB_WORKBOY_SHIFT_UP);
        }
        else {
            GB_workboy_set_key(_gb, GB_WORKBOY_NONE);
        }

    }
    bool handled = false;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    unsigned player_count = GB_get_player_count(_gb);
    if (self.document.partner) {
        player_count = 2;
    }
    for (unsigned player = 0; player < player_count; player++) {
        for (GBButton button = 0; button < GBKeyboardButtonCount; button++) {
            NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)];
            if (!key) continue;
            
            if (key.unsignedShortValue == keyCode) {
                handled = true;
                switch (button) {
                    case GBTurbo:
                        if (self.document.isSlave) {
                            GB_set_turbo_mode(self.document.partner.gb, false, false);
                        }
                        else {
                            GB_set_turbo_mode(_gb, false, false);
                        }
                        _turbo = false;
                        analogClockMultiplierValid = false;
                        break;
                        
                    case GBRewind:
                        self.isRewinding = false;
                        break;
                        
                    case GBUnderclock:
                        underclockKeyDown = false;
                        analogClockMultiplierValid = false;
                        break;
                    
                    case GBRapidA:
                        _rapidA[player] = false;
                        GB_set_key_state_for_player(_gb, GB_KEY_A, player, false);
                        break;
                        
                    case GBRapidB:
                        _rapidB[player] = false;
                        GB_set_key_state_for_player(_gb, GB_KEY_B, player, false);
                        break;
                        
                    default:
                        if (self.document.partner) {
                            if (player == 0) {
                                GB_set_key_state_for_player(_gb, (GB_key_t)button, 0, false);
                            }
                            else {
                                GB_set_key_state_for_player(self.document.partner.gb, (GB_key_t)button, 0, false);
                            }
                        }
                        else {
                            GB_set_key_state_for_player(_gb, (GB_key_t)button, player, false);
                        }
                        break;
                }
            }
        }
    }
    if (!handled && [theEvent type] != NSEventTypeFlagsChanged) {
        [super keyUp:theEvent];
    }
}

- (void)setRumble:(double)amp
{
    [lastController setRumbleAmplitude:amp];
}

- (bool)shouldControllerUseJoystickForMotion:(JOYController *)controller
{
    if (!_gb) return false;
    if (!GB_has_accelerometer(_gb)) return false;
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7JoystickOverride"]) return true;
    for (JOYAxes3D *axes in controller.axes3D) {
        if (axes.usage == JOYAxes3DUsageOrientation || axes.usage == JOYAxes3DUsageAcceleration) {
            return false;
        }
    }
    return true;
}

- (bool)allowController
{
    if ([self.window isMainWindow]) return true;
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAllowBackgroundControllers"]) {
        if ([(Document *)[NSApplication sharedApplication].orderedDocuments.firstObject mainWindow] == self.window) {
            return true;
        }
    }
    return false;
}

- (void)controller:(JOYController *)controller movedAxis:(JOYAxis *)axis
{
    if (!_gb) return;
    if (![self allowController]) return;

    NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
    if (!mapping) {
        mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName];
    }
    
    if ((axis.usage == JOYAxisUsageR1 && !mapping) ||
        axis.uniqueID == [mapping[@"AnalogUnderclock"] unsignedLongValue]){
        analogClockMultiplier = MIN(MAX(1 - axis.value + 0.05, 1.0 / 3), 1.0);
        analogClockMultiplierValid = true;
    }
    
    else if ((axis.usage == JOYAxisUsageL1 && !mapping) ||
        axis.uniqueID == [mapping[@"AnalogTurbo"] unsignedLongValue]){
        analogClockMultiplier = MIN(MAX(axis.value * 3 + 0.95, 1.0), 3.0);
        analogClockMultiplierValid = true;
    }
}

- (bool)controller:(JOYController *)controller applicableForPlayer:(unsigned)player effectivePlayer:(unsigned *)effectivePlayer effectiveGB:(GB_gameboy_t **)effectiveGB
{
    NSDictionary<NSNumber *, JOYController *> *controllerMapping = [self controllerMapping];

    JOYController *preferredJoypad = controllerMapping[@(player)];
    if (preferredJoypad && preferredJoypad != controller) return false; // The player has a different assigned controller
    if (!preferredJoypad && self.playerCount != 1) return false; // The player has no assigned controller in multiplayer mode, prevent controller inputs
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [controller setPlayerLEDs:[controller LEDMaskForPlayer:player]];
    });
    
    *effectiveGB = _gb;
    *effectivePlayer = player;
    
    if (player && self.document.partner) {
        *effectiveGB = self.document.partner.gb;
        *effectivePlayer = 0;
        if (controller != self.document.partner.view->lastController) {
            [self setRumble:0];
            self.document.partner.view->lastController = controller;
        }
    }
    else {
        if (controller != lastController) {
            [self setRumble:0];
            lastController = controller;
        }
    }
    return true;
}

- (void)controller:(JOYController *)controller movedAxes2D:(JOYAxes2D *)axes
{
    if (!_gb) return;
    /* Always handle only the most dominant 2D input. */
    for (JOYAxes2D *otherAxes in controller.axes2D) {
        if (otherAxes == axes) continue;
        if (otherAxes.distance > axes.distance) {
            return;
        }
    }
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    if ([self shouldControllerUseJoystickForMotion:controller] && !self.mouseControlsActive) {
        GB_set_accelerometer_values(_gb, -axes.value.x, -axes.value.y);
    }
    else if ([defaults boolForKey:@"GBFauxAnalogInputs"]) {
        unsigned playerCount = self.playerCount;
        for (unsigned player = 0; player < playerCount; player++) {
            unsigned effectivePlayer;
            GB_gameboy_t *effectiveGB;
            if (![self controller:controller applicableForPlayer:player effectivePlayer:&effectivePlayer effectiveGB:&effectiveGB]) continue;
            
            GB_set_use_faux_analog_inputs(effectiveGB, effectivePlayer, true);
            NSPoint position = axes.value;
            GB_set_faux_analog_inputs(effectiveGB, effectivePlayer, position.x, position.y);
        }
    }
}

- (void)controller:(JOYController *)controller movedAxes3D:(JOYAxes3D *)axes
{
    if (!_gb) return;
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7JoystickOverride"]) return;
    if (self.mouseControlsActive) return;
    if (controller != lastController) return;
    // When using Joy-Cons in dual-controller grip, ignore motion data from the left Joy-Con
    if (controller.joyconType == JOYJoyConTypeDual) {
        for (JOYController *child in [(JOYCombinedController *)controller children]) {
            if (child.joyconType != JOYJoyConTypeRight && [child.axes3D containsObject:axes]) {
                return;
            }
        }
    }
    
    NSDictionary<NSNumber *, JOYController *> *controllerMapping = [self controllerMapping];
    GB_gameboy_t *effectiveGB = _gb;
    
    if (self.document.partner) {
        if (controllerMapping[@1] == controller) {
            effectiveGB = self.document.partner.gb;
        }
        if (controllerMapping[@0] != controller) {
            return;
        }
        
    }

    if (axes.usage == JOYAxes3DUsageOrientation) {
        for (JOYAxes3D *axes in controller.axes3D) {
            // Only use orientation if there's no acceleration axes
            if (axes.usage == JOYAxes3DUsageAcceleration) {
                return;
            }
        }
        JOYPoint3D point = axes.normalizedValue;
        GB_set_accelerometer_values(effectiveGB, point.x, point.z);
    }
    else if (axes.usage == JOYAxes3DUsageAcceleration) {
        JOYPoint3D point = axes.gUnitsValue;
        GB_set_accelerometer_values(effectiveGB, point.x, point.z);
    }
}

- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button
{
    if (!_gb) return;
    if (![self allowController]) return;
    _mouseControlEnabled = false;
    if (button.type == JOYButtonTypeAxes2DEmulated && [self shouldControllerUseJoystickForMotion:controller]) return;
    
    [self tryAssigningController:controller];
    
    unsigned playerCount = self.playerCount;

    IOPMAssertionID assertionID;
    IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID);
    
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    bool fauxAnalog = [defaults boolForKey:@"GBFauxAnalogInputs"];

    for (unsigned player = 0; player < playerCount; player++) {
        unsigned effectivePlayer;
        GB_gameboy_t *effectiveGB;
        if (![self controller:controller applicableForPlayer:player effectivePlayer:&effectivePlayer effectiveGB:&effectiveGB]) continue;
        
        NSDictionary *mapping = [defaults dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
        if (!mapping) {
            mapping = [defaults dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName];
        }
        
        JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: button.usage;
        if (!mapping && usage >= JOYButtonUsageGeneric0) {
            usage = GB_inline_const(JOYButtonUsage[], {JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX})[(usage - JOYButtonUsageGeneric0) & 3];
        }
        
        if (usage >= JOYButtonUsageDPadLeft && usage <= JOYButtonUsageDPadDown) {
            if (fauxAnalog && button.type == JOYButtonTypeAxes2DEmulated) {
                // This isn't a real button, it's an emulated Axes2D. We want to handle it as an Axes2D instead
                continue;
            }
            else {
                // User used a digital direction input, revert to non-analog inputs
                GB_set_use_faux_analog_inputs(effectiveGB, effectivePlayer, false);
            }
        }
        
        switch ((unsigned)usage) {
                
            case JOYButtonUsageNone: break;
            case JOYButtonUsageA: GB_set_key_state_for_player(effectiveGB, GB_KEY_A, effectivePlayer, button.isPressed); break;
            case JOYButtonUsageB: GB_set_key_state_for_player(effectiveGB, GB_KEY_B, effectivePlayer, button.isPressed); break;
            case JOYButtonUsageC: break;
            case JOYButtonUsageStart:
            case JOYButtonUsageX: GB_set_key_state_for_player(effectiveGB, GB_KEY_START, effectivePlayer, button.isPressed); break;
            case JOYButtonUsageSelect:
            case JOYButtonUsageY: GB_set_key_state_for_player(effectiveGB, GB_KEY_SELECT, effectivePlayer, button.isPressed); break;
            case JOYButtonUsageR2:
            case JOYButtonUsageL2:
            case JOYButtonUsageZ: {
                self.isRewinding = button.isPressed;
                if (button.isPressed) {
                    if (self.document.isSlave) {
                        GB_set_turbo_mode(self.document.partner.gb, false, false);
                    }
                    else {
                        GB_set_turbo_mode(_gb, false, false);
                    }
                    _turbo = false;
                }
                break;
            }
        
            case JOYButtonUsageL1: {
                if (!analogClockMultiplierValid || analogClockMultiplier == 1.0 || !button.isPressed) {
                    if (self.document.isSlave) {
                        GB_set_turbo_mode(self.document.partner.gb, button.isPressed, false);
                    }
                    else {
                        GB_set_turbo_mode(_gb, button.isPressed, button.isPressed && self.isRewinding);
                    }
                    _turbo = button.isPressed;
                }
                break;
            }

            case JOYButtonUsageR1: underclockKeyDown = button.isPressed; break;
            case JOYButtonUsageDPadLeft: GB_set_key_state_for_player(effectiveGB, GB_KEY_LEFT, effectivePlayer, button.isPressed); break;
            case JOYButtonUsageDPadRight: GB_set_key_state_for_player(effectiveGB, GB_KEY_RIGHT, effectivePlayer, button.isPressed); break;
            case JOYButtonUsageDPadUp: GB_set_key_state_for_player(effectiveGB, GB_KEY_UP, effectivePlayer, button.isPressed); break;
            case JOYButtonUsageDPadDown: GB_set_key_state_for_player(effectiveGB, GB_KEY_DOWN, effectivePlayer, button.isPressed); break;

            case GBJoyKitRapidA:
                _rapidA[effectivePlayer] = button.isPressed;
                _rapidACount[effectivePlayer] = 0;
                GB_set_key_state_for_player(_gb, GB_KEY_A, effectivePlayer, button.isPressed);
                break;
                
            case GBJoyKitRapidB:
                _rapidB[effectivePlayer] = button.isPressed;
                _rapidBCount[effectivePlayer] = 0;
                GB_set_key_state_for_player(_gb, GB_KEY_B, effectivePlayer, button.isPressed);
                break;
        }
    }
}

- (BOOL)acceptsFirstResponder
{
    return true;
}

- (bool)mouseControlsActive
{
    return _gb && GB_is_inited(_gb) && GB_has_accelerometer(_gb) &&
           _mouseControlEnabled && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7AllowMouse"];
}

- (void)mouseEntered:(NSEvent *)theEvent
{
    if (!mouse_hidden) {
        mouse_hidden = true;
        if (_mouseHidingEnabled &&
            !self.mouseControlsActive) {
            [NSCursor hide];
        }
    }
    [super mouseEntered:theEvent];
}

- (void)mouseExited:(NSEvent *)theEvent
{
    if (mouse_hidden) {
        mouse_hidden = false;
        if (_mouseHidingEnabled) {
            [NSCursor unhide];
        }
    }
    [super mouseExited:theEvent];
}

- (void)mouseDown:(NSEvent *)event
{
    _mouseControlEnabled = true;
    if (self.mouseControlsActive) {
        if (event.type == NSEventTypeLeftMouseDown) {
            GB_set_key_state(_gb, GB_KEY_A, true);
        }
    }
}

- (void)mouseUp:(NSEvent *)event
{
    if (self.mouseControlsActive) {
        if (event.type == NSEventTypeLeftMouseUp) {
            GB_set_key_state(_gb, GB_KEY_A, false);
        }
    }
}

- (void)mouseMoved:(NSEvent *)event
{
    if (self.mouseControlsActive) {
        NSPoint point = [self convertPoint:[event locationInWindow] toView:nil];
        
        point.x /= self.frame.size.width;
        point.x *= 2;
        point.x -= 1;
        
        point.y /= self.frame.size.height;
        point.y *= 2;
        point.y -= 1;
        
        if (GB_get_screen_width(_gb) != 160) { // has border
            point.x *= 256 / 160.0;
            point.y *= 224 / 114.0;
        }
        GB_set_accelerometer_values(_gb, -point.x, point.y);
    }
}

- (void)setMouseHidingEnabled:(bool)mouseHidingEnabled
{
    if (mouseHidingEnabled == _mouseHidingEnabled) return;

    _mouseHidingEnabled = mouseHidingEnabled;
    
    if (mouse_hidden && _mouseHidingEnabled) {
        [NSCursor hide];
    }

    if (mouse_hidden && !_mouseHidingEnabled) {
        [NSCursor unhide];
    }
}

- (bool)isMouseHidingEnabled
{
    return _mouseHidingEnabled;
}

- (void) flagsChanged:(NSEvent *)event
{
    if (event.modifierFlags > previousModifiers) {
        [self keyDown:event];
    }
    else {
        [self keyUp:event];
    }
    
    previousModifiers = event.modifierFlags;
}

-(NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
    NSPasteboard *pboard = [sender draggingPasteboard];
    
    if ( [[pboard types] containsObject:NSURLPboardType] ) {
        NSURL *fileURL = [NSURL URLFromPasteboard:pboard];
        if (GB_is_save_state(fileURL.fileSystemRepresentation)) {
            return NSDragOperationGeneric;
        }
    }
    return NSDragOperationNone;
}

-(BOOL)performDragOperation:(id<NSDraggingInfo>)sender
{
    NSPasteboard *pboard = [sender draggingPasteboard];
    
    if ( [[pboard types] containsObject:NSURLPboardType] ) {
        NSURL *fileURL = [NSURL URLFromPasteboard:pboard];
        return [_document loadStateFile:fileURL.fileSystemRepresentation noErrorOnNotFound:false];
    }

    return false;
}

- (NSImage *)renderToImage;
{
    /* Not going to support this on OpenGL, OpenGL is too much of a terrible API for me
       to bother figuring out how the hell something so trivial can be done. */
    return nil;
}
@end
