#import <Carbon/Carbon.h>
#import "GBView.h"
#import "GBViewGL.h"
#import "GBViewMetal.h"
#import "GBButtons.h"
#import "NSString+StringForKey.h"

#define JOYSTICK_HIGH 0x4000
#define JOYSTICK_LOW 0x3800

@implementation GBView
{
    uint32_t *image_buffers[3];
    unsigned char current_buffer;
    BOOL mouse_hidden;
    NSTrackingArea *tracking_area;
    BOOL _mouseHidingEnabled;
    bool axisActive[2];
    bool underclockKeyDown;
    double clockMultiplier;
    NSEventModifierFlags previousModifiers;
}

+ (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) createInternalView
{
    assert(false && "createInternalView must not be inherited");
}

- (void) _init
{    
    _shouldBlendFrameWithPrevious = 1;
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ratioKeepingChanged) name:@"GBAspectChanged" object:nil];
    tracking_area = [ [NSTrackingArea alloc] initWithRect:(NSRect){}
                                                  options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect
                                                    owner:self
                                                 userInfo:nil];
    [self addTrackingArea:tracking_area];
    clockMultiplier = 1.0;
    [self createInternalView];
    [self addSubview:self.internalView];
    self.internalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
}

- (void)screenSizeChanged
{
    if (image_buffers[0]) free(image_buffers[0]);
    if (image_buffers[1]) free(image_buffers[1]);
    if (image_buffers[2]) free(image_buffers[2]);
    
    size_t buffer_size = sizeof(image_buffers[0][0]) * GB_get_screen_width(_gb) * GB_get_screen_height(_gb);
    
    image_buffers[0] = calloc(1, buffer_size);
    image_buffers[1] = calloc(1, buffer_size);
    image_buffers[2] = calloc(1, buffer_size);
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self setFrame:self.superview.frame];
    });
}

- (void) ratioKeepingChanged
{
    [self setFrame:self.superview.frame];
}

- (void) setShouldBlendFrameWithPrevious:(BOOL)shouldBlendFrameWithPrevious
{
    _shouldBlendFrameWithPrevious = shouldBlendFrameWithPrevious;
    [self setNeedsDisplay:YES];
}

- (unsigned char) numberOfBuffers
{
    return _shouldBlendFrameWithPrevious? 3 : 2;
}

- (void)dealloc
{
    free(image_buffers[0]);
    free(image_buffers[1]);
    free(image_buffers[2]);
    if (mouse_hidden) {
        mouse_hidden = false;
        [NSCursor unhide];
    }
    [[NSNotificationCenter defaultCenter] removeObserver: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
{
    frame = self.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;
        }
    }

    [super setFrame:frame];
}

- (void) flip
{
    if (underclockKeyDown && clockMultiplier > 0.5) {
        clockMultiplier -= 1.0/16;
        GB_set_clock_multiplier(_gb, clockMultiplier);
    }
    if (!underclockKeyDown && clockMultiplier < 1.0) {
        clockMultiplier += 1.0/16;
        GB_set_clock_multiplier(_gb, clockMultiplier);
    }
    current_buffer = (current_buffer + 1) % self.numberOfBuffers;
}

- (uint32_t *) pixels
{
    return image_buffers[(current_buffer + 1) % self.numberOfBuffers];
}

-(void)keyDown:(NSEvent *)theEvent
{
    unsigned short keyCode = theEvent.keyCode;
    bool handled = false;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    unsigned player_count = GB_get_player_count(_gb);
    for (unsigned player = 0; player < player_count; player++) {
        for (GBButton button = 0; button < GBButtonCount; button++) {
            NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)];
            if (!key) continue;

            if (key.unsignedShortValue == keyCode) {
                handled = true;
                switch (button) {
                    case GBTurbo:
                        GB_set_turbo_mode(_gb, true, self.isRewinding);
                        break;
                        
                    case GBRewind:
                        self.isRewinding = true;
                        GB_set_turbo_mode(_gb, false, false);
                        break;
                        
                    case GBUnderclock:
                        underclockKeyDown = true;
                        break;
                        
                    default:
                        GB_set_key_state_for_player(_gb, (GB_key_t)button, player, true);
                        break;
                }
            }
        }
    }

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

-(void)keyUp:(NSEvent *)theEvent
{
    unsigned short keyCode = theEvent.keyCode;
    bool handled = false;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    unsigned player_count = GB_get_player_count(_gb);
    for (unsigned player = 0; player < player_count; player++) {
        for (GBButton button = 0; button < GBButtonCount; button++) {
            NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)];
            if (!key) continue;
            
            if (key.unsignedShortValue == keyCode) {
                handled = true;
                switch (button) {
                    case GBTurbo:
                        GB_set_turbo_mode(_gb, false, false);
                        break;
                        
                    case GBRewind:
                        self.isRewinding = false;
                        break;
                        
                    case GBUnderclock:
                        underclockKeyDown = false;
                        break;
                        
                    default:
                        GB_set_key_state_for_player(_gb, (GB_key_t)button, player, false);
                        break;
                }
            }
        }
    }
    if (!handled && [theEvent type] != NSEventTypeFlagsChanged) {
        [super keyUp:theEvent];
    }
}

- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state
{
    unsigned player_count = GB_get_player_count(_gb);

    UpdateSystemActivity(UsrActivity);
    for (unsigned player = 0; player < player_count; player++) {
        NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"]
                                      objectForKey:[NSString stringWithFormat:@"%u", player]];
        if (player_count != 1 && // Single player, accpet inputs from all joypads
            !(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads
            ![preferred_joypad isEqualToString:joystick_name]) {
            continue;
        }
        NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name];
        
        for (GBButton i = 0; i < GBButtonCount; i++) {
            NSNumber *mapped_button = [mapping objectForKey:GBButtonNames[i]];
            if (mapped_button && [mapped_button integerValue] == button) {
                switch (i) {
                    case GBTurbo:
                        GB_set_turbo_mode(_gb, state, state && self.isRewinding);
                        break;
                        
                    case GBRewind:
                        self.isRewinding = state;
                        if (state) {
                            GB_set_turbo_mode(_gb, false, false);
                        }
                        break;
                    
                    case GBUnderclock:
                        underclockKeyDown = state;
                        break;
                        
                    default:
                        GB_set_key_state_for_player(_gb, (GB_key_t)i, player, state);
                        break;
                }
            }
        }
    }
}

- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value
{
    unsigned player_count = GB_get_player_count(_gb);

    UpdateSystemActivity(UsrActivity);
    for (unsigned player = 0; player < player_count; player++) {
        NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"]
                                      objectForKey:[NSString stringWithFormat:@"%u", player]];
        if (player_count != 1 && // Single player, accpet inputs from all joypads
            !(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads
            ![preferred_joypad isEqualToString:joystick_name]) {
            continue;
        }
        
        NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name];
        NSNumber *x_axis = [mapping objectForKey:@"XAxis"];
        NSNumber *y_axis = [mapping objectForKey:@"YAxis"];
        
        if (axis == [x_axis integerValue]) {
            if (value > JOYSTICK_HIGH) {
                axisActive[0] = true;
                GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, true);
                GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, false);
            }
            else if (value < -JOYSTICK_HIGH) {
                axisActive[0] = true;
                GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, false);
                GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, true);
            }
            else if (axisActive[0] && value < JOYSTICK_LOW && value > -JOYSTICK_LOW) {
                axisActive[0] = false;
                GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, false);
                GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, false);
            }
        }
        else if (axis == [y_axis integerValue]) {
            if (value > JOYSTICK_HIGH) {
                axisActive[1] = true;
                GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, true);
                GB_set_key_state_for_player(_gb, GB_KEY_UP, player, false);
            }
            else if (value < -JOYSTICK_HIGH) {
                axisActive[1] = true;
                GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, false);
                GB_set_key_state_for_player(_gb, GB_KEY_UP, player, true);
            }
            else if (axisActive[1] && value < JOYSTICK_LOW && value > -JOYSTICK_LOW) {
                axisActive[1] = false;
                GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, false);
                GB_set_key_state_for_player(_gb, GB_KEY_UP, player, false);
            }
        }
    }
}

- (void) joystick:(NSString *)joystick_name hat: (unsigned)hat changedState: (int8_t) state
{
    unsigned player_count = GB_get_player_count(_gb);
    
    UpdateSystemActivity(UsrActivity);
    for (unsigned player = 0; player < player_count; player++) {
        NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"]
                                      objectForKey:[NSString stringWithFormat:@"%u", player]];
        if (player_count != 1 && // Single player, accpet inputs from all joypads
            !(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads
            ![preferred_joypad isEqualToString:joystick_name]) {
            continue;
        }
        assert(state + 1  < 9);
                                                                      /* -  N NE  E SE  S SW  W NW */
        GB_set_key_state_for_player(_gb, GB_KEY_UP,    player, (bool []){0, 1, 1, 0, 0, 0, 0, 0, 1}[state + 1]);
        GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, (bool []){0, 0, 1, 1, 1, 0, 0, 0, 0}[state + 1]);
        GB_set_key_state_for_player(_gb, GB_KEY_DOWN,  player, (bool []){0, 0, 0, 0, 1, 1, 1, 0, 0}[state + 1]);
        GB_set_key_state_for_player(_gb, GB_KEY_LEFT,  player, (bool []){0, 0, 0, 0, 0, 0, 1, 1, 1}[state + 1]);
    }
}

- (BOOL)acceptsFirstResponder
{
    return YES;
}

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

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

- (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;
}

- (uint32_t *)currentBuffer
{
    return image_buffers[current_buffer];
}

- (uint32_t *)previousBuffer
{
    return image_buffers[(current_buffer + 2) % self.numberOfBuffers];
}

@end
