#import <AVFoundation/AVFoundation.h>
#import <CoreAudio/CoreAudio.h>
#import <Core/gb.h>
#import "GBAudioClient.h"
#import "Document.h"
#import "AppDelegate.h"
#import "HexFiend/HexFiend.h"
#import "GBMemoryByteArray.h"
#import "GBWarningPopover.h"
#import "GBCheatWindowController.h"
#import "GBTerminalTextFieldCell.h"
#import "BigSurToolbar.h"
#import "GBPaletteEditorController.h"
#import "GBObjectView.h"
#import "GBPaletteView.h"

@implementation NSString (relativePath)

- (NSString *)pathRelativeToDirectory:(NSString *)directory
{
    NSMutableArray<NSString *> *baseComponents = [[directory pathComponents] mutableCopy];
    NSMutableArray<NSString *> *selfComponents = [[self pathComponents] mutableCopy];
    
    while (baseComponents.count) {
        if (![baseComponents.firstObject isEqualToString:selfComponents.firstObject]) {
            break;
        }
        
        [baseComponents removeObjectAtIndex:0];
        [selfComponents removeObjectAtIndex:0];
    }
    while (baseComponents.count) {
        [baseComponents removeObjectAtIndex:0];
        [selfComponents insertObject:@".." atIndex:0];
    }
    return [selfComponents componentsJoinedByString:@"/"];
}

@end

#define GB_MODEL_PAL_BIT_OLD 0x1000

/* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */
/* Todo: Split into category files! This is so messy!!! */

enum model {
    MODEL_NONE,
    MODEL_DMG,
    MODEL_CGB,
    MODEL_AGB,
    MODEL_SGB,
    MODEL_MGB,
};

@interface Document ()
{
    
    NSMutableAttributedString *pending_console_output;
    NSRecursiveLock *console_output_lock;
    NSTimer *console_output_timer;
    NSTimer *hex_timer;
    
    bool fullScreen;
    bool in_sync_input;
    HFController *hex_controller;

    NSString *lastConsoleInput;
    HFLineCountingRepresenter *lineRep;

    CVImageBufferRef cameraImage;
    AVCaptureSession *cameraSession;
    AVCaptureConnection *cameraConnection;
    AVCaptureStillImageOutput *cameraOutput;
    
    GB_oam_info_t _oamInfo[40];
    
    NSMutableData *currentPrinterImageData;
    enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy, GBAccessoryLinkCable} accessory;
    
    bool rom_warning_issued;
    
    NSMutableString *capturedOutput;
    bool logToSideView;
    bool shouldClearSideView;
    enum model current_model;
    
    bool rewind;
    bool modelsChanging;
    
    NSCondition *audioLock;
    GB_sample_t *audioBuffer;
    size_t audioBufferSize;
    size_t audioBufferPosition;
    size_t audioBufferNeeded;
    double _volume;
    
    bool borderModeChanged;
    
    /* Link cable*/
    Document *master;
    Document *slave;
    signed linkOffset;
    bool linkCableBit;
    
    NSSavePanel *_audioSavePanel;
    bool _isRecordingAudio;
}

@property GBAudioClient *audioClient;
- (void) vblank;
- (void) log: (const char *) log withAttributes: (GB_log_attributes) attributes;
- (char *) getDebuggerInput;
- (char *) getAsyncDebuggerInput;
- (void) cameraRequestUpdate;
- (uint8_t) cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y;
- (void) printImage:(uint32_t *)image height:(unsigned) height
          topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
           exposure:(unsigned) exposure;
- (void) gotNewSample:(GB_sample_t *)sample;
- (void) rumbleChanged:(double)amp;
- (void) loadBootROM:(GB_boot_rom_t)type;
- (void)linkCableBitStart:(bool)bit;
- (bool)linkCableBitEnd;
- (void)infraredStateChanged:(bool)state;

@end

static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self loadBootROM: type];
}

static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self vblank];
}

static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self log:string withAttributes: attributes];
}

static char *consoleInput(GB_gameboy_t *gb)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    return [self getDebuggerInput];
}

static char *asyncConsoleInput(GB_gameboy_t *gb)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    char *ret = [self getAsyncDebuggerInput];
    return ret;
}

static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
{
    return (r << 0) | (g << 8) | (b << 16) | 0xFF000000;
}

static void cameraRequestUpdate(GB_gameboy_t *gb)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self cameraRequestUpdate];
}

static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    return [self cameraGetPixelAtX:x andY:y];
}

static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height,
                       uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure];
}

static void setWorkboyTime(GB_gameboy_t *gb, time_t t)
{
    [[NSUserDefaults standardUserDefaults] setInteger:time(NULL) - t forKey:@"GBWorkboyTimeOffset"];
}

static time_t getWorkboyTime(GB_gameboy_t *gb)
{
    return time(NULL) - [[NSUserDefaults standardUserDefaults] integerForKey:@"GBWorkboyTimeOffset"];
}

static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self gotNewSample:sample];
}

static void rumbleCallback(GB_gameboy_t *gb, double amp)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self rumbleChanged:amp];
}


static void linkCableBitStart(GB_gameboy_t *gb, bool bit_to_send)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self linkCableBitStart:bit_to_send];
}

static bool linkCableBitEnd(GB_gameboy_t *gb)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    return [self linkCableBitEnd];
}

static void infraredStateChanged(GB_gameboy_t *gb, bool on)
{
    Document *self = (__bridge Document *)GB_get_user_data(gb);
    [self infraredStateChanged:on];
}


@implementation Document
{
    GB_gameboy_t gb;
    volatile bool running;
    volatile bool stopping;
    NSConditionLock *has_debugger_input;
    NSMutableArray *debugger_input_queue;
}

- (instancetype)init 
{
    self = [super init];
    if (self) {
        has_debugger_input = [[NSConditionLock alloc] initWithCondition:0];
        debugger_input_queue = [[NSMutableArray alloc] init];
        console_output_lock = [[NSRecursiveLock alloc] init];
        audioLock = [[NSCondition alloc] init];
        _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"];
    }
    return self;
}

- (NSString *)bootROMPathForName:(NSString *)name
{
    NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"];
    if (url) {
        NSString *path = [url path];
        path = [path stringByAppendingPathComponent:name];
        path = [path stringByAppendingPathExtension:@"bin"];
        if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
            return path;
        }
    }
    
    return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"];
}

- (GB_model_t)internalModel
{
    switch (current_model) {
        case MODEL_DMG:
            return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"];
            
        case MODEL_NONE:
        case MODEL_CGB:
            return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"];
            
        case MODEL_SGB: {
            GB_model_t model = (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"];
            if (model == (GB_MODEL_SGB | GB_MODEL_PAL_BIT_OLD)) {
                model = GB_MODEL_SGB_PAL;
            }
            return model;
        }
        
        case MODEL_MGB:
            return GB_MODEL_MGB;
        
        case MODEL_AGB:
            return GB_MODEL_AGB;
    }
}

- (void) updatePalette
{
    GB_set_palette(&gb, [GBPaletteEditorController userPalette]);
}

- (void) updateBorderMode
{
    borderModeChanged = true;
}

- (void) updateRumbleMode
{
    GB_set_rumble_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRumbleMode"]);
}

- (void) initCommon
{
    GB_init(&gb, [self internalModel]);
    GB_set_user_data(&gb, (__bridge void *)(self));
    GB_set_boot_rom_load_callback(&gb, (GB_boot_rom_load_callback_t)boot_rom_load);
    GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank);
    GB_set_log_callback(&gb, (GB_log_callback_t) consoleLog);
    GB_set_input_callback(&gb, (GB_input_callback_t) consoleInput);
    GB_set_async_input_callback(&gb, (GB_input_callback_t) asyncConsoleInput);
    GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]);
    GB_set_light_temperature(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]);
    GB_set_interference_volume(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]);
    GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
    [self updatePalette];
    GB_set_rgb_encode_callback(&gb, rgbEncode);
    GB_set_camera_get_pixel_callback(&gb, cameraGetPixel);
    GB_set_camera_update_request_callback(&gb, cameraRequestUpdate);
    GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]);
    GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]);
    GB_set_rtc_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]);
    GB_apu_set_sample_callback(&gb, audioCallback);
    GB_set_rumble_callback(&gb, rumbleCallback);
    GB_set_infrared_callback(&gb, infraredStateChanged);
    [self updateRumbleMode];
}

- (void) updateMinSize
{
    self.mainWindow.contentMinSize = NSMakeSize(GB_get_screen_width(&gb), GB_get_screen_height(&gb));
    if (self.mainWindow.contentView.bounds.size.width < GB_get_screen_width(&gb) ||
        self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&gb)) {
        [self.mainWindow zoom:nil];
    }
    self.osdView.usesSGBScale = GB_get_screen_width(&gb) == 256;
}

- (void) vblank
{
    if (_gbsVisualizer) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [_gbsVisualizer setNeedsDisplay:true];
        });
    }
    [self.view flip];
    if (borderModeChanged) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            size_t previous_width = GB_get_screen_width(&gb);
            GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
            if (GB_get_screen_width(&gb) != previous_width) {
                [self.view screenSizeChanged];
                [self updateMinSize];
            }
        });
        borderModeChanged = false;
    }
    GB_set_pixels_output(&gb, self.view.pixels);
    if (self.vramWindow.isVisible) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0;
            [self reloadVRAMData: nil];
        });
    }
    if (self.view.isRewinding) {
        rewind = true;
        [self.osdView displayText:@"Rewinding..."];
    }
}

- (void)gotNewSample:(GB_sample_t *)sample
{
    if (_gbsVisualizer) {
        [_gbsVisualizer addSample:sample];
    }
    [audioLock lock];
    if (_audioClient.isPlaying) {
        if (audioBufferPosition == audioBufferSize) {
            if (audioBufferSize >= 0x4000) {
                audioBufferPosition = 0;
                [audioLock unlock];
                return;
            }
            
            if (audioBufferSize == 0) {
                audioBufferSize = 512;
            }
            else {
                audioBufferSize += audioBufferSize >> 2;
            }
            audioBuffer = realloc(audioBuffer, sizeof(*sample) * audioBufferSize);
        }
        if (_volume != 1) {
            sample->left *= _volume;
            sample->right *= _volume;
        }
        audioBuffer[audioBufferPosition++] = *sample;
    }
    if (audioBufferPosition == audioBufferNeeded) {
        [audioLock signal];
        audioBufferNeeded = 0;
    }
    [audioLock unlock];
}

- (void)rumbleChanged:(double)amp
{
    [_view setRumble:amp];
}

- (void) preRun
{
    GB_set_pixels_output(&gb, self.view.pixels);
    GB_set_sample_rate(&gb, 96000);
    _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) {
        [audioLock lock];
        
        if (audioBufferPosition < nFrames) {
            audioBufferNeeded = nFrames;
            [audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.125]];
        }
        
        if (stopping || GB_debugger_is_stopped(&gb)) {
            memset(buffer, 0, nFrames * sizeof(*buffer));
            [audioLock unlock];
            return;
        }
        
        if (audioBufferPosition < nFrames) {
            // Not enough audio
            memset(buffer, 0, (nFrames - audioBufferPosition) * sizeof(*buffer));
            memcpy(buffer, audioBuffer, audioBufferPosition * sizeof(*buffer));
            audioBufferPosition = 0;
        }
        else if (audioBufferPosition < nFrames + 4800) {
            memcpy(buffer, audioBuffer, nFrames * sizeof(*buffer));
            memmove(audioBuffer, audioBuffer + nFrames, (audioBufferPosition - nFrames) * sizeof(*buffer));
            audioBufferPosition = audioBufferPosition - nFrames;
        }
        else {
            memcpy(buffer, audioBuffer + (audioBufferPosition - nFrames), nFrames * sizeof(*buffer));
            audioBufferPosition = 0;
        }
        [audioLock unlock];
    } andSampleRate:96000];
    if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) {
        [_audioClient start];
    }
    hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:true];
    [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode];
    
    /* Clear pending alarms, don't play alarms while playing */
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
        NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
        for (NSUserNotification *notification in [center scheduledNotifications]) {
            if ([notification.identifier isEqualToString:self.fileURL.path]) {
                [center removeScheduledNotification:notification];
                break;
            }
        }
        
        for (NSUserNotification *notification in [center deliveredNotifications]) {
            if ([notification.identifier isEqualToString:self.fileURL.path]) {
                [center removeDeliveredNotification:notification];
                break;
            }
        }
    }
}

static unsigned *multiplication_table_for_frequency(unsigned frequency)
{
    unsigned *ret = malloc(sizeof(*ret) * 0x100);
    for (unsigned i = 0; i < 0x100; i++) {
        ret[i] = i * frequency;
    }
    return ret;
}

- (void) run
{
    assert(!master);
    [self preRun];
    if (slave) {
        [slave preRun];
        unsigned *masterTable = multiplication_table_for_frequency(GB_get_clock_rate(&gb));
        unsigned *slaveTable = multiplication_table_for_frequency(GB_get_clock_rate(&slave->gb));
        while (running) {
            if (linkOffset <= 0) {
                linkOffset += slaveTable[GB_run(&gb)];
            }
            else {
                linkOffset -= masterTable[GB_run(&slave->gb)];
            }
        }
        free(masterTable);
        free(slaveTable);
        [slave postRun];
    }
    else {
        while (running) {
            if (rewind) {
                rewind = false;
                GB_rewind_pop(&gb);
                if (!GB_rewind_pop(&gb)) {
                    rewind = self.view.isRewinding;
                }
            }
            else {
                GB_run(&gb);
            }
        }
    }
    [self postRun];
    stopping = false;
}

- (void)postRun
{
    [hex_timer invalidate];
    [audioLock lock];
    memset(audioBuffer, 0, (audioBufferSize - audioBufferPosition) * sizeof(*audioBuffer));
    audioBufferPosition = audioBufferNeeded;
    [audioLock signal];
    [audioLock unlock];
    [_audioClient stop];
    _audioClient = nil;
    self.view.mouseHidingEnabled = false;
    GB_save_battery(&gb, self.savPath.UTF8String);
    GB_save_cheats(&gb, self.chtPath.UTF8String);
    unsigned time_to_alarm = GB_time_to_alarm(&gb);
    
    if (time_to_alarm) {
        [NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate];
        NSUserNotification *notification = [[NSUserNotification alloc] init];
        NSString *friendlyName = [[self.fileURL lastPathComponent] stringByDeletingPathExtension];
        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil];
        friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""];
        friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        
        notification.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName];
        notification.informativeText = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName];
        notification.identifier = self.fileURL.path;
        notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm];
        notification.soundName = NSUserNotificationDefaultSoundName;
        [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification];
        [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"];
    }
    [_view setRumble:0];
}

- (void) start
{
    self.gbsPlayPauseButton.state = true;
    self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0;
    if (master) {
        [master start];
        return;
    }
    if (running) return;
    running = true;
    [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
}

- (void) stop
{
    self.gbsPlayPauseButton.state = false;
    if (master) {
        if (!master->running) return;
        GB_debugger_set_disabled(&gb, true);
        if (GB_debugger_is_stopped(&gb)) {
            [self interruptDebugInputRead];
        }
        [master stop];
        GB_debugger_set_disabled(&gb, false);
        return;
    }
    if (!running) return;
    GB_debugger_set_disabled(&gb, true);
    if (GB_debugger_is_stopped(&gb)) {
        [self interruptDebugInputRead];
    }
    [audioLock lock];
    stopping = true;
    [audioLock signal];
    [audioLock unlock];
    running = false;
    while (stopping) {
        [audioLock lock];
        [audioLock signal];
        [audioLock unlock];
    }
    GB_debugger_set_disabled(&gb, false);
}

- (void) loadBootROM: (GB_boot_rom_t)type
{
    static NSString *const names[] = {
        [GB_BOOT_ROM_DMG_0] = @"dmg0_boot",
        [GB_BOOT_ROM_DMG] = @"dmg_boot",
        [GB_BOOT_ROM_MGB] = @"mgb_boot",
        [GB_BOOT_ROM_SGB] = @"sgb_boot",
        [GB_BOOT_ROM_SGB2] = @"sgb2_boot",
        [GB_BOOT_ROM_CGB_0] = @"cgb0_boot",
        [GB_BOOT_ROM_CGB] = @"cgb_boot",
        [GB_BOOT_ROM_AGB] = @"agb_boot",
    };
    GB_load_boot_rom(&gb, [[self bootROMPathForName:names[type]] UTF8String]);
}

- (IBAction)reset:(id)sender
{
    [self stop];
    size_t old_width = GB_get_screen_width(&gb);
    
    if ([sender tag] != MODEL_NONE) {
        current_model = (enum model)[sender tag];
    }
    
    GB_switch_model_and_reset(&gb, [self internalModel]);
    
    if (old_width != GB_get_screen_width(&gb)) {
        [self.view screenSizeChanged];
    }
    
    [self updateMinSize];
    
    if ([sender tag] != 0) {
        /* User explictly selected a model, save the preference */
        [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_DMG forKey:@"EmulateDMG"];
        [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_SGB forKey:@"EmulateSGB"];
        [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_AGB forKey:@"EmulateAGB"];
        [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_MGB forKey:@"EmulateMGB"];
    }
    
    /* Reload the ROM, SAV and SYM files */
    [self loadROM];

    [self start];

    if (hex_controller) {
        /* Verify bank sanity, especially when switching models. */
        [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:0];
        [self hexUpdateBank:self.memoryBankInput ignoreErrors:true];
    }
    
    char title[17];
    GB_get_rom_title(&gb, title);
    [self.osdView displayText:[NSString stringWithFormat:@"SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&gb)]];
}

- (IBAction)togglePause:(id)sender
{
    if (master) {
        [master togglePause:sender];
        return;
    }
    if (running) {
        [self stop];
    }
    else {
        [self start];
    }
}

- (void)dealloc
{
    [cameraSession stopRunning];
    self.view.gb = NULL;
    GB_free(&gb);
    if (cameraImage) {
        CVBufferRelease(cameraImage);
    }
    if (audioBuffer) {
        free(audioBuffer);
    }
}

- (void)windowControllerDidLoadNib:(NSWindowController *)aController 
{
    [super windowControllerDidLoadNib:aController];
    // Interface Builder bug?
    [self.consoleWindow setContentSize:self.consoleWindow.minSize];
    /* Close Open Panels, if any */
    for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
        if ([window isKindOfClass:[NSOpenPanel class]]) {
            [(NSOpenPanel *)window cancel:self];
        }
    }
    
    NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init];
    [paragraph_style setLineSpacing:2];
        
    self.debuggerSideViewInput.font = [NSFont userFixedPitchFontOfSize:12];
    self.debuggerSideViewInput.textColor = [NSColor whiteColor];
    self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style;
    [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"];
    ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &gb;
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateSideView)
                                                 name:NSTextDidChangeNotification
                                               object:self.debuggerSideViewInput];
    
    self.consoleOutput.textContainerInset = NSMakeSize(4, 4);
    [self.view becomeFirstResponder];
    self.view.frameBlendingMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"];
    CGRect window_frame = self.mainWindow.frame;
    window_frame.size.width  = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowWidth"],
                                  window_frame.size.width);
    window_frame.size.height = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowHeight"],
                                   window_frame.size.height);
    [self.mainWindow setFrame:window_frame display:true];
    self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised;
        
    NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height;
    CGRect vram_window_rect = self.vramWindow.frame;
    vram_window_rect.size.height = 384 + height_diff + 48;
    [self.vramWindow setFrame:vram_window_rect display:true animate:false];
    
    
    self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]];
    self.debuggerSplitView.dividerColor = [NSColor clearColor];
    if (@available(macOS 11.0, *)) {
        self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded;
        self.printerFeedWindow.toolbarStyle = NSWindowToolbarStyleUnifiedCompact;
        [self.printerFeedWindow.toolbar removeItemAtIndex:1];
        self.printerFeedWindow.toolbar.items.firstObject.image =
            [NSImage imageWithSystemSymbolName:@"square.and.arrow.down"
                      accessibilityDescription:@"Save"];
        self.printerFeedWindow.toolbar.items.lastObject.image =
            [NSImage imageWithSystemSymbolName:@"printer"
                      accessibilityDescription:@"Print"];
    }
        
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateHighpassFilter)
                                                 name:@"GBHighpassFilterChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateColorCorrectionMode)
                                                 name:@"GBColorCorrectionChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateLightTemperature)
                                                 name:@"GBLightTemperatureChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateInterferenceVolume)
                                                 name:@"GBInterferenceVolumeChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateFrameBlendingMode)
                                                 name:@"GBFrameBlendingModeChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updatePalette)
                                                 name:@"GBColorPaletteChanged"
                                               object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateBorderMode)
                                                 name:@"GBBorderModeChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateRumbleMode)
                                                 name:@"GBRumbleModeChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateRewindLength)
                                                 name:@"GBRewindLengthChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateRTCMode)
                                                 name:@"GBRTCModeChanged"
                                               object:nil];

    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(dmgModelChanged)
                                                 name:@"GBDMGModelChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(sgbModelChanged)
                                                 name:@"GBSGBModelChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(cgbModelChanged)
                                                 name:@"GBCGBModelChanged"
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateVolume)
                                                 name:@"GBVolumeChanged"
                                               object:nil];
        
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) {
        current_model = MODEL_DMG;
    }
    else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateSGB"]) {
        current_model = MODEL_SGB;
    }
    else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateMGB"]) {
        current_model = MODEL_MGB;
    }
    else {
        current_model = [[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateAGB"]? MODEL_AGB : MODEL_CGB;
    }
    
    [self initCommon];
    self.view.gb = &gb;
    self.view.osdView = _osdView;
    [self.view screenSizeChanged];
    if ([self loadROM]) {
        _mainWindow.alphaValue = 0; // Hack hack ugly hack
        dispatch_async(dispatch_get_main_queue(), ^{
            [self close];
        });
    }
    else {
        [self reset:nil];
    }
}

- (void) initMemoryView
{
    hex_controller = [[HFController alloc] init];
    [hex_controller setBytesPerColumn:1];
    [hex_controller setEditMode:HFOverwriteMode];
    
    [hex_controller setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]];

    /* Here we're going to make three representers - one for the hex, one for the ASCII, and one for the scrollbar.  To lay these all out properly, we'll use a fourth HFLayoutRepresenter. */
    HFLayoutRepresenter *layoutRep = [[HFLayoutRepresenter alloc] init];
    HFHexTextRepresenter *hexRep = [[HFHexTextRepresenter alloc] init];
    HFStringEncodingTextRepresenter *asciiRep = [[HFStringEncodingTextRepresenter alloc] init];
    HFVerticalScrollerRepresenter *scrollRep = [[HFVerticalScrollerRepresenter alloc] init];
    lineRep = [[HFLineCountingRepresenter alloc] init];
    HFStatusBarRepresenter *statusRep = [[HFStatusBarRepresenter alloc] init];

    lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal;

    /* Add all our reps to the controller. */
    [hex_controller addRepresenter:layoutRep];
    [hex_controller addRepresenter:hexRep];
    [hex_controller addRepresenter:asciiRep];
    [hex_controller addRepresenter:scrollRep];
    [hex_controller addRepresenter:lineRep];
    [hex_controller addRepresenter:statusRep];

    /* Tell the layout rep which reps it should lay out. */
    [layoutRep addRepresenter:hexRep];
    [layoutRep addRepresenter:scrollRep];
    [layoutRep addRepresenter:asciiRep];
    [layoutRep addRepresenter:lineRep];
    [layoutRep addRepresenter:statusRep];


    [(NSView *)[hexRep view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];

    /* Grab the layout rep's view and stick it into our container. */
    NSView *layoutView = [layoutRep view];
    NSRect layoutViewFrame = self.memoryView.frame;
    [layoutView setFrame:layoutViewFrame];
    [layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin];
    [self.memoryView addSubview:layoutView];

    self.memoryBankItem.enabled = false;
}

+ (BOOL)autosavesInPlace 
{
    return true;
}

- (NSString *)windowNibName 
{
    // Override returning the nib file name of the document
    // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead.
    return @"Document";
}

- (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type
{
    return true;
}

- (IBAction)changeGBSTrack:(id)sender
{
    if (!running) {
        [self start];
    }
    [self performAtomicBlock:^{
        GB_gbs_switch_track(&gb, self.gbsTracks.indexOfSelectedItem);
    }];
}
- (IBAction)gbsNextPrevPushed:(id)sender
{
    if (self.gbsNextPrevButton.selectedSegment == 0) {
        // Previous
        if (self.gbsTracks.indexOfSelectedItem == 0) {
            [self.gbsTracks selectItemAtIndex:self.gbsTracks.numberOfItems - 1];
        }
        else {
            [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem - 1];
        }
    }
    else {
        // Next
        if (self.gbsTracks.indexOfSelectedItem == self.gbsTracks.numberOfItems - 1) {
            [self.gbsTracks selectItemAtIndex: 0];
        }
        else {
            [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem + 1];
        }
    }
    [self changeGBSTrack:sender];
}

- (void)prepareGBSInterface: (GB_gbs_info_t *)info
{
    GB_set_rendering_disabled(&gb, true);
    _view = nil;
    for (NSView *view in [_mainWindow.contentView.subviews copy]) {
        [view removeFromSuperview];
    }
    [[NSBundle mainBundle] loadNibNamed:@"GBS" owner:self topLevelObjects:nil];
    [_mainWindow setContentSize:self.gbsPlayerView.bounds.size];
    _mainWindow.styleMask &= ~NSWindowStyleMaskResizable;
    dispatch_async(dispatch_get_main_queue(), ^{ // Cocoa is weird, no clue why it's needed
        [_mainWindow standardWindowButton:NSWindowZoomButton].enabled = false;
    });
    [_mainWindow.contentView addSubview:self.gbsPlayerView];
    _mainWindow.movableByWindowBackground = true;
    [_mainWindow setContentBorderThickness:24 forEdge:NSRectEdgeMinY];

    self.gbsTitle.stringValue = [NSString stringWithCString:info->title encoding:NSISOLatin1StringEncoding] ?: @"GBS Player";
    self.gbsAuthor.stringValue = [NSString stringWithCString:info->author encoding:NSISOLatin1StringEncoding] ?: @"Unknown Composer";
    NSString *copyright = [NSString stringWithCString:info->copyright encoding:NSISOLatin1StringEncoding];
    if (copyright) {
        copyright = [@"©" stringByAppendingString:copyright];
    }
    self.gbsCopyright.stringValue = copyright ?: @"Missing copyright information";
    for (unsigned i = 0; i < info->track_count; i++) {
        [self.gbsTracks addItemWithTitle:[NSString stringWithFormat:@"Track %u", i + 1]];
    }
    [self.gbsTracks selectItemAtIndex:info->first_track];
    self.gbsPlayPauseButton.image.template = true;
    self.gbsPlayPauseButton.alternateImage.template = true;
    self.gbsRewindButton.image.template = true;
    for (unsigned i = 0; i < 2; i++) {
        [self.gbsNextPrevButton imageForSegment:i].template = true;
    }

    if (!_audioClient.isPlaying) {
        [_audioClient start];
    }
    
    if (@available(macOS 10.10, *)) {
        _mainWindow.titlebarAppearsTransparent = true;
    }
}

- (bool)isCartContainer
{
    return [self.fileName.pathExtension.lowercaseString isEqualToString:@"gbcart"];
}

- (NSString *)savPath
{
    if (self.isCartContainer) {
        return [self.fileName stringByAppendingPathComponent:@"battery.sav"];
    }
    
    return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path;
}

- (NSString *)chtPath
{
    if (self.isCartContainer) {
        return [self.fileName stringByAppendingPathComponent:@"cheats.cht"];
    }
    
    return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path;
}

- (NSString *)saveStatePath:(unsigned)index
{
    if (self.isCartContainer) {
        return [self.fileName stringByAppendingPathComponent:[NSString stringWithFormat:@"state.s%u", index]];
    }
    return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%u", index]].path;
}

- (NSString *)romPath
{
    NSString *fileName = self.fileName;
    if (self.isCartContainer) {
        NSArray *paths = [[NSString stringWithContentsOfFile:[fileName stringByAppendingPathComponent:@"rom.gbl"]
                                                    encoding:NSUTF8StringEncoding
                                                       error:nil] componentsSeparatedByString:@"\n"];
        fileName = nil;
        bool needsRebuild = false;
        for (NSString *path in paths) {
            NSURL *url = [NSURL URLWithString:path relativeToURL:self.fileURL];
            if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
                if (fileName && ![fileName isEqualToString:url.path]) {
                    needsRebuild = true;
                    break;
                }
                fileName = url.path;
            }
            else {
                needsRebuild = true;
            }
        }
        if (fileName && needsRebuild) {
            [[NSString stringWithFormat:@"%@\n%@\n%@",
              [fileName pathRelativeToDirectory:self.fileName],
              fileName,
              [[NSURL fileURLWithPath:fileName].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")]]
             writeToFile:[self.fileName stringByAppendingPathComponent:@"rom.gbl"]
             atomically:false
             encoding:NSUTF8StringEncoding
             error:nil];
        }
    }
    
    return fileName;
}

- (int) loadROM
{
    __block int ret = 0;
    NSString *fileName = self.romPath;
    if (!fileName) {
        NSAlert *alert = [[NSAlert alloc] init];
        [alert setMessageText:@"Could not locate the ROM referenced by this Game Boy Cartridge"];
        [alert setAlertStyle:NSAlertStyleCritical];
        [alert runModal];
        return 1;
    }
    
    NSString *rom_warnings = [self captureOutputForBlock:^{
        GB_debugger_clear_symbols(&gb);
        if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"isx"]) {
            ret = GB_load_isx(&gb, fileName.UTF8String);
            if (!self.isCartContainer) {
                GB_load_battery(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"ram"].path.UTF8String);
            }
        }
        else if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"gbs"]) {
            __block GB_gbs_info_t info;
            ret = GB_load_gbs(&gb, fileName.UTF8String, &info);
            [self prepareGBSInterface:&info];
        }
        else {
            ret = GB_load_rom(&gb, [fileName UTF8String]);
        }
        GB_load_battery(&gb, self.savPath.UTF8String);
        GB_load_cheats(&gb, self.chtPath.UTF8String);
        [self.cheatWindowController cheatsUpdated];
        GB_debugger_load_symbol_file(&gb, [[[NSBundle mainBundle] pathForResource:@"registers" ofType:@"sym"] UTF8String]);
        GB_debugger_load_symbol_file(&gb, [[fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sym"].UTF8String);
    }];
    if (ret) {
        NSAlert *alert = [[NSAlert alloc] init];
        [alert setMessageText:rom_warnings?: @"Could not load ROM"];
        [alert setAlertStyle:NSAlertStyleCritical];
        [alert runModal];
    }
    else if (rom_warnings && !rom_warning_issued) {
        rom_warning_issued = true;
        [GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow];
    }
    return ret;
}

- (void)close
{
    [self disconnectLinkCable];
    if (!self.gbsPlayerView) {
        [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"];
        [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"];
    }
    [self stop];
    [self.consoleWindow close];
    [self.memoryWindow close];
    [self.vramWindow close];
    [self.printerFeedWindow close];
    [self.cheatsWindow close];
    [super close];
}

- (IBAction) interrupt:(id)sender
{
    [self log:"^C\n"];
    GB_debugger_break(&gb);
    [self start];
    [self.consoleWindow makeKeyAndOrderFront:nil];
    [self.consoleInput becomeFirstResponder];
}

- (IBAction)mute:(id)sender
{
    if (_audioClient.isPlaying) {
        [_audioClient stop];
    }
    else {
        [_audioClient start];
        if (_volume == 0) {
            [GBWarningPopover popoverWithContents:@"Warning: Volume is set to to zero in the preferences panel" onWindow:self.mainWindow];
        }
    }
    [[NSUserDefaults standardUserDefaults] setBool:!_audioClient.isPlaying forKey:@"Mute"];
}

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem
{
    if ([anItem action] == @selector(mute:)) {
        [(NSMenuItem *)anItem setState:!_audioClient.isPlaying];
    }
    else if ([anItem action] == @selector(togglePause:)) {
        if (master) {
            [(NSMenuItem *)anItem setState:(!master->running) || (GB_debugger_is_stopped(&gb)) || (GB_debugger_is_stopped(&gb))];
        }
        [(NSMenuItem *)anItem setState:(!running) || (GB_debugger_is_stopped(&gb))];
        return !GB_debugger_is_stopped(&gb);
    }
    else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE) {
        [(NSMenuItem *)anItem setState:anItem.tag == current_model];
    }
    else if ([anItem action] == @selector(interrupt:)) {
        if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
            return false;
        }
    }
    else if ([anItem action] == @selector(disconnectAllAccessories:)) {
        [(NSMenuItem *)anItem setState:accessory == GBAccessoryNone];
    }
    else if ([anItem action] == @selector(connectPrinter:)) {
        [(NSMenuItem *)anItem setState:accessory == GBAccessoryPrinter];
    }
    else if ([anItem action] == @selector(connectWorkboy:)) {
        [(NSMenuItem *)anItem setState:accessory == GBAccessoryWorkboy];
    }
    else if ([anItem action] == @selector(connectLinkCable:)) {
        [(NSMenuItem *)anItem setState:[(NSMenuItem *)anItem representedObject] == master ||
                                       [(NSMenuItem *)anItem representedObject] == slave];
    }
    else if ([anItem action] == @selector(toggleCheats:)) {
        [(NSMenuItem *)anItem setState:GB_cheats_enabled(&gb)];
    }
    else if ([anItem action] == @selector(toggleDisplayBackground:)) {
        [(NSMenuItem *)anItem setState:!GB_is_background_rendering_disabled(&gb)];
    }
    else if ([anItem action] == @selector(toggleDisplayObjects:)) {
        [(NSMenuItem *)anItem setState:!GB_is_object_rendering_disabled(&gb)];
    }
    else if ([anItem action] == @selector(toggleAudioRecording:)) {
        [(NSMenuItem *)anItem setTitle:_isRecordingAudio? @"Stop Audio Recording" : @"Start Audio Recording…"];
    }
    
    return [super validateUserInterfaceItem:anItem];
}


- (void) windowWillEnterFullScreen:(NSNotification *)notification
{
    fullScreen = true;
    self.view.mouseHidingEnabled = running;
}

- (void) windowWillExitFullScreen:(NSNotification *)notification
{
    fullScreen = false;
    self.view.mouseHidingEnabled = false;
}

- (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame
{
    if (fullScreen) {
        return newFrame;
    }
    size_t width  = GB_get_screen_width(&gb),
           height = GB_get_screen_height(&gb);
    
    NSRect rect = window.contentView.frame;

    unsigned titlebarSize = window.contentView.superview.frame.size.height - rect.size.height;
    unsigned step = width / [[window screen] backingScaleFactor];

    rect.size.width = floor(rect.size.width / step) * step + step;
    rect.size.height = rect.size.width * height / width + titlebarSize;

    if (rect.size.width > newFrame.size.width) {
        rect.size.width = width;
        rect.size.height = height + titlebarSize;
    }
    else if (rect.size.height > newFrame.size.height) {
        rect.size.width = width;
        rect.size.height = height + titlebarSize;
    }

    rect.origin = window.frame.origin;
    rect.origin.y -= rect.size.height - window.frame.size.height;

    return rect;
}

- (void) appendPendingOutput
{
    [console_output_lock lock];
    if (shouldClearSideView) {
        shouldClearSideView = false;
        [self.debuggerSideView setString:@""];
    }
    if (pending_console_output) {
        NSTextView *textView = logToSideView? self.debuggerSideView : self.consoleOutput;
        
        [hex_controller reloadData];
        [self reloadVRAMData: nil];
        
        [textView.textStorage appendAttributedString:pending_console_output];
        [textView scrollToEndOfDocument:nil];
        if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
            [self.consoleWindow orderFront:nil];
        }
        pending_console_output = nil;
    }
    [console_output_lock unlock];
}

- (void) log: (const char *) string withAttributes: (GB_log_attributes) attributes
{
    NSString *nsstring = @(string); // For ref-counting
    if (capturedOutput) {
        [capturedOutput appendString:nsstring];
        return;
    }
    
    
    NSFont *font = [NSFont userFixedPitchFontOfSize:12];
    NSUnderlineStyle underline = NSUnderlineStyleNone;
    if (attributes & GB_LOG_BOLD) {
        font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask];
    }
    
    if (attributes &  GB_LOG_UNDERLINE_MASK) {
        underline = (attributes &  GB_LOG_UNDERLINE_MASK) == GB_LOG_DASHED_UNDERLINE? NSUnderlinePatternDot | NSUnderlineStyleSingle : NSUnderlineStyleSingle;
    }
    
    NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init];
    [paragraph_style setLineSpacing:2];
    NSMutableAttributedString *attributed =
    [[NSMutableAttributedString alloc] initWithString:nsstring
                                           attributes:@{NSFontAttributeName: font,
                                                        NSForegroundColorAttributeName: [NSColor whiteColor],
                                                        NSUnderlineStyleAttributeName: @(underline),
                                                        NSParagraphStyleAttributeName: paragraph_style}];
    
    [console_output_lock lock];
    if (!pending_console_output) {
        pending_console_output = attributed;
    }
    else {
        [pending_console_output appendAttributedString:attributed];
    }
    
    if (![console_output_timer isValid]) {
        console_output_timer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:false];
        [[NSRunLoop mainRunLoop] addTimer:console_output_timer forMode:NSDefaultRunLoopMode];
    }
    
    [console_output_lock unlock];

    /* Make sure mouse is not hidden while debugging */
    self.view.mouseHidingEnabled = false;
}

- (IBAction)showConsoleWindow:(id)sender
{
    [self.consoleWindow orderBack:nil];
}

- (IBAction)consoleInput:(NSTextField *)sender 
{
    NSString *line = [sender stringValue];
    if ([line isEqualToString:@""] && lastConsoleInput) {
        line = lastConsoleInput;
    }
    else if (line) {
        lastConsoleInput = line;
    }
    else {
        line = @"";
    }

    if (!in_sync_input) {
        [self log:">"];
    }
    [self log:[line UTF8String]];
    [self log:"\n"];
    [has_debugger_input lock];
    [debugger_input_queue addObject:line];
    [has_debugger_input unlockWithCondition:1];

    [sender setStringValue:@""];
}

- (void) interruptDebugInputRead
{
    [has_debugger_input lock];
    [debugger_input_queue addObject:[NSNull null]];
    [has_debugger_input unlockWithCondition:1];
}

- (void) updateSideView
{
    if (!GB_debugger_is_stopped(&gb)) {
        return;
    }
    
    if (![NSThread isMainThread]) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self updateSideView];
        });
        return;
    }
    
    [console_output_lock lock];
    shouldClearSideView = true;
    [self appendPendingOutput];
    logToSideView = true;
    [console_output_lock unlock];
    
    for (NSString *line in [self.debuggerSideViewInput.string componentsSeparatedByString:@"\n"]) {
        NSString *stripped = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        if ([stripped length]) {
            char *dupped = strdup([stripped UTF8String]);
            GB_attributed_log(&gb, GB_LOG_BOLD, "%s:\n", dupped);
            GB_debugger_execute_command(&gb, dupped);
            GB_log(&gb, "\n");
            free(dupped);
        }
    }
    
    [console_output_lock lock];
    [self appendPendingOutput];
    logToSideView = false;
    [console_output_lock unlock];
}

- (char *) getDebuggerInput
{
    bool isPlaying = _audioClient.isPlaying;
    if (isPlaying) {
        [_audioClient stop];
    }
    [audioLock lock];
    [audioLock signal];
    [audioLock unlock];
    [self updateSideView];
    [self log:">"];
    in_sync_input = true;
    [has_debugger_input lockWhenCondition:1];
    NSString *input = [debugger_input_queue firstObject];
    [debugger_input_queue removeObjectAtIndex:0];
    [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0];
    in_sync_input = false;
    shouldClearSideView = true;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC / 10)), dispatch_get_main_queue(), ^{
        if (shouldClearSideView) {
            shouldClearSideView = false;
            [self.debuggerSideView setString:@""];
        }
    });
    if (isPlaying) {
        [_audioClient start];
    }
    if ((id) input == [NSNull null]) {
        return NULL;
    }
    return strdup([input UTF8String]);
}

- (char *) getAsyncDebuggerInput
{
    [has_debugger_input lock];
    NSString *input = [debugger_input_queue firstObject];
    if (input) {
        [debugger_input_queue removeObjectAtIndex:0];
    }
    [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0];
    if ((id)input == [NSNull null]) {
        return NULL;
    }
    return input? strdup([input UTF8String]): NULL;
}

- (IBAction)saveState:(id)sender
{
    bool __block success = false;
    [self performAtomicBlock:^{
        success = GB_save_state(&gb, [self saveStatePath:[sender tag]].UTF8String) == 0;
    }];
    
    if (!success) {
        [GBWarningPopover popoverWithContents:@"Failed to write save state." onWindow:self.mainWindow];
        NSBeep();
    }
    else {
        [self.osdView displayText:@"State saved"];
    }
}

- (int)loadStateFile:(const char *)path noErrorOnNotFound:(bool)noErrorOnFileNotFound;
{
    int __block result = false;
    NSString *error =
    [self captureOutputForBlock:^{
        result = GB_load_state(&gb, path);
    }];
    
    if (result == ENOENT && noErrorOnFileNotFound) {
        return ENOENT;
    }
    
    if (result) {
        NSBeep();
    }
    else {
        [self.osdView displayText:@"State loaded"];
    }
    if (error) {
        [GBWarningPopover popoverWithContents:error onWindow:self.mainWindow];
    }
    return result;
}

- (IBAction)loadState:(id)sender
{
    int ret = [self loadStateFile:[self saveStatePath:[sender tag]].UTF8String noErrorOnNotFound:true];
    if (ret == ENOENT && !self.isCartContainer) {
        [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"sn%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:false];
    }
}

- (IBAction)clearConsole:(id)sender
{
    [self.consoleOutput setString:@""];
}

- (void)log:(const char *)log
{
    [self log:log withAttributes:0];
}

- (uint8_t) readMemory:(uint16_t)addr
{
    while (!GB_is_inited(&gb));
    return GB_safe_read_memory(&gb, addr);
}

- (void) writeMemory:(uint16_t)addr value:(uint8_t)value
{
    while (!GB_is_inited(&gb));
    GB_write_memory(&gb, addr, value);
}

- (void) performAtomicBlock: (void (^)())block
{
    while (!GB_is_inited(&gb));
    bool was_running = running && !GB_debugger_is_stopped(&gb);
    if (master) {
        was_running |= master->running;
    }
    if (was_running) {
        [self stop];
    }
    block();
    if (was_running) {
        [self start];
    }
}

- (NSString *) captureOutputForBlock: (void (^)())block
{
    capturedOutput = [[NSMutableString alloc] init];
    [self performAtomicBlock:block];
    NSString *ret = [capturedOutput stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
    capturedOutput = nil;
    return [ret length]? ret : nil;
}

+ (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale
{
    
    NSImage *ret = [[NSImage alloc] initWithSize:NSMakeSize(width * scale, height * scale)];
    NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
                                                                    pixelsWide:width
                                                                    pixelsHigh:height
                                                                 bitsPerSample:8
                                                               samplesPerPixel:3
                                                                      hasAlpha:false
                                                                      isPlanar:false
                                                                colorSpaceName:NSDeviceRGBColorSpace
                                                                  bitmapFormat:0
                                                                   bytesPerRow:4 * width
                                                                  bitsPerPixel:32];
    memcpy(rep.bitmapData, data.bytes, data.length);
    [ret addRepresentation:rep];
    return ret;
}

- (void) reloadMemoryView
{
    if (self.memoryWindow.isVisible) {
        [hex_controller reloadData];
    }
}

- (IBAction) reloadVRAMData: (id) sender
{
    if (self.vramWindow.isVisible) {
        uint8_t *io_regs = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL);
        switch ([self.vramTabView.tabViewItems indexOfObject:self.vramTabView.selectedTabViewItem]) {
            case 0:
            /* Tileset */
            {
                GB_palette_type_t palette_type = GB_PALETTE_NONE;
                NSUInteger palette_menu_index = self.tilesetPaletteButton.indexOfSelectedItem;
                if (palette_menu_index) {
                    palette_type = palette_menu_index > 8? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND;
                }
                size_t bufferLength = 256 * 192 * 4;
                NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength];
                data.length = bufferLength;
                GB_draw_tileset(&gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 1) & 7);
                
                self.tilesetImageView.image = [Document imageFromData:data width:256 height:192 scale:1.0];
                self.tilesetImageView.layer.magnificationFilter = kCAFilterNearest;
            }
            break;
                
            case 1:
            /* Tilemap */
            {
                GB_palette_type_t palette_type = GB_PALETTE_NONE;
                NSUInteger palette_menu_index = self.tilemapPaletteButton.indexOfSelectedItem;
                if (palette_menu_index > 1) {
                    palette_type = palette_menu_index > 9? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND;
                }
                else if (palette_menu_index == 1) {
                    palette_type = GB_PALETTE_AUTO;
                }
                
                size_t bufferLength = 256 * 256 * 4;
                NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength];
                data.length = bufferLength;
                GB_draw_tilemap(&gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 2) & 7,
                                (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem,
                                (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem);
                
                self.tilemapImageView.scrollRect = NSMakeRect(io_regs[GB_IO_SCX],
                                                              io_regs[GB_IO_SCY],
                                                              160, 144);
                self.tilemapImageView.image = [Document imageFromData:data width:256 height:256 scale:1.0];
                self.tilemapImageView.layer.magnificationFilter = kCAFilterNearest;
            }
            break;
                
            case 2:
            /* OAM */
            {
                _oamCount = GB_get_oam_info(&gb, _oamInfo, &_oamHeight);
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.objectView reloadData:self];
                });
            }
            break;
            
            case 3:
            /* Palettes */
            {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.paletteView reloadData:self];
                });
            }
            break;
        }
    }
}

- (IBAction) showMemory:(id)sender
{
    if (!hex_controller) {
        [self initMemoryView];
    }
    [self.memoryWindow makeKeyAndOrderFront:sender];
}

- (IBAction)hexGoTo:(id)sender
{
    NSString *error = [self captureOutputForBlock:^{
        uint16_t addr;
        if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, NULL)) {
            return;
        }
        addr -= lineRep.valueOffset;
        if (addr >= hex_controller.byteArray.length) {
            GB_log(&gb, "Value $%04x is out of range.\n", addr);
            return;
        }
        [hex_controller setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]];
        [hex_controller _ensureVisibilityOfLocation:addr];
        [self.memoryWindow makeFirstResponder:self.memoryView.subviews[0].subviews[0]];
    }];
    if (error) {
        NSBeep();
        [GBWarningPopover popoverWithContents:error onView:sender];
    }
}

- (void)hexUpdateBank:(NSControl *)sender ignoreErrors: (bool)ignore_errors
{
    NSString *error = [self captureOutputForBlock:^{
        uint16_t addr, bank;
        if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, &bank)) {
            return;
        }

        if (bank == (uint16_t) -1) {
            bank = addr;
        }

        uint16_t n_banks = 1;
        switch ([(GBMemoryByteArray *)(hex_controller.byteArray) mode]) {
            case GBMemoryROM: {
                size_t rom_size;
                GB_get_direct_access(&gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL);
                n_banks = rom_size / 0x4000;
                break;
            }
            case GBMemoryVRAM:
                n_banks = GB_is_cgb(&gb) ? 2 : 1;
                break;
            case GBMemoryExternalRAM: {
                size_t ram_size;
                GB_get_direct_access(&gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL);
                n_banks = (ram_size + 0x1FFF) / 0x2000;
                break;
            }
            case GBMemoryRAM:
                n_banks = GB_is_cgb(&gb) ? 8 : 1;
                break;
            case GBMemoryEntireSpace:
                break;
        }

        bank %= n_banks;

        [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]];
        [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:bank];
        [hex_controller reloadData];
    }];
    
    if (error && !ignore_errors) {
        NSBeep();
        [GBWarningPopover popoverWithContents:error onView:sender];
    }
}

- (IBAction)hexUpdateBank:(NSControl *)sender
{
    [self hexUpdateBank:sender ignoreErrors:false];
}

- (IBAction)hexUpdateSpace:(NSPopUpButtonCell *)sender
{
    self.memoryBankItem.enabled = [sender indexOfSelectedItem] != GBMemoryEntireSpace;
    GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(hex_controller.byteArray);
    [byteArray setMode:(GB_memory_mode_t)[sender indexOfSelectedItem]];
    uint16_t bank;
    switch ((GB_memory_mode_t)[sender indexOfSelectedItem]) {
        case GBMemoryEntireSpace:
        case GBMemoryROM:
            lineRep.valueOffset = 0;
            GB_get_direct_access(&gb, GB_DIRECT_ACCESS_ROM, NULL, &bank);
            byteArray.selectedBank = bank;
            break;
        case GBMemoryVRAM:
            lineRep.valueOffset = 0x8000;
            GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, &bank);
            byteArray.selectedBank = bank;
            break;
        case GBMemoryExternalRAM:
            lineRep.valueOffset = 0xA000;
            GB_get_direct_access(&gb, GB_DIRECT_ACCESS_CART_RAM, NULL, &bank);
            byteArray.selectedBank = bank;
            break;
        case GBMemoryRAM:
            lineRep.valueOffset = 0xC000;
            GB_get_direct_access(&gb, GB_DIRECT_ACCESS_RAM, NULL, &bank);
            byteArray.selectedBank = bank;
            break;
    }
    [self.memoryBankInput setStringValue:[NSString stringWithFormat:@"$%x", byteArray.selectedBank]];
    [hex_controller reloadData];
    [self.memoryView setNeedsDisplay:true];
}

- (GB_gameboy_t *) gameboy
{
    return &gb;
}

+ (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName
{
    return true;
}

- (void)cameraRequestUpdate
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @try {
            if (!cameraSession) {
                if (@available(macOS 10.14, *)) {
                    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
                        case AVAuthorizationStatusAuthorized:
                            break;
                        case AVAuthorizationStatusNotDetermined: {
                            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                                [self cameraRequestUpdate];
                            }];
                            return;
                        }
                        case AVAuthorizationStatusDenied:
                        case AVAuthorizationStatusRestricted:
                            GB_camera_updated(&gb);
                            return;
                    }
                }

                NSError *error;
                AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
                AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error];
                CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions([[[device formats] firstObject] formatDescription]);

                if (!input) {
                    GB_camera_updated(&gb);
                    return;
                }

                cameraOutput = [[AVCaptureStillImageOutput alloc] init];
                /* Greyscale is not widely supported, so we use YUV, whose first element is the brightness. */
                [cameraOutput setOutputSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kYUVSPixelFormat),
                                                   (id)kCVPixelBufferWidthKey: @(MAX(128, 112 * dimensions.width / dimensions.height)),
                                                   (id)kCVPixelBufferHeightKey: @(MAX(112, 128 * dimensions.height / dimensions.width)),}];


                cameraSession = [AVCaptureSession new];
                cameraSession.sessionPreset = AVCaptureSessionPresetPhoto;

                [cameraSession addInput: input];
                [cameraSession addOutput: cameraOutput];
                [cameraSession startRunning];
                cameraConnection = [cameraOutput connectionWithMediaType: AVMediaTypeVideo];
            }

            [cameraOutput captureStillImageAsynchronouslyFromConnection: cameraConnection completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError *error) {
                if (error) {
                    GB_camera_updated(&gb);
                }
                else {
                    if (cameraImage) {
                        CVBufferRelease(cameraImage);
                        cameraImage = NULL;
                    }
                    cameraImage = CVBufferRetain(CMSampleBufferGetImageBuffer(sampleBuffer));
                    /* We only need the actual buffer, no need to ever unlock it. */
                    CVPixelBufferLockBaseAddress(cameraImage, 0);
                }
                
                GB_camera_updated(&gb);
            }];
        }
        @catch (NSException *exception) {
            /* I have not tested camera support on many devices, so we catch exceptions just in case. */
            GB_camera_updated(&gb);
        }
    });
}

- (uint8_t)cameraGetPixelAtX:(uint8_t)x andY:(uint8_t) y
{
    if (!cameraImage) {
        return 0;
    }

    uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(cameraImage);
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(cameraImage);
    uint8_t offsetX = (CVPixelBufferGetWidth(cameraImage) - 128) / 2;
    uint8_t offsetY = (CVPixelBufferGetHeight(cameraImage) - 112) / 2;
    uint8_t ret = baseAddress[(x + offsetX) * 2 + (y + offsetY) * bytesPerRow];

    return ret;
}

- (IBAction)toggleTilesetGrid:(NSButton *)sender
{
    if (sender.state) {
        self.tilesetImageView.horizontalGrids = @[
                                                  [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
                                                  [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:128],
                                                  
        ];
        self.tilesetImageView.verticalGrids = @[
                                                  [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
                                                  [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:64],
        ];
        self.tilemapImageView.horizontalGrids = @[
                                                  [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
                                                  ];
        self.tilemapImageView.verticalGrids = @[
                                                [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
                                                ];
    }
    else {
        self.tilesetImageView.horizontalGrids = nil;
        self.tilesetImageView.verticalGrids = nil;
        self.tilemapImageView.horizontalGrids = nil;
        self.tilemapImageView.verticalGrids = nil;
    }
}

- (IBAction)toggleScrollingDisplay:(NSButton *)sender
{
    self.tilemapImageView.displayScrollRect = sender.state;
}

- (IBAction)vramTabChanged:(NSSegmentedControl *)sender
{
    [self.vramTabView selectTabViewItemAtIndex:[sender selectedSegment]];
    [self reloadVRAMData:sender];
    [self.vramTabView.selectedTabViewItem.view addSubview:self.gridButton];
    self.gridButton.hidden = [sender selectedSegment] >= 2;

    NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height;
    CGRect window_rect = self.vramWindow.frame;
    window_rect.origin.y += window_rect.size.height;
    switch ([sender selectedSegment]) {
        case 0:
        case 2:
            window_rect.size.height = 384 + height_diff + 48;
            break;
        case 1:
            window_rect.size.height = 512 + height_diff + 48;
            break;
        case 3:
            window_rect.size.height = 24 * 16 + height_diff;
            break;
            
        default:
            break;
    }
    window_rect.origin.y -= window_rect.size.height;
    [self.vramWindow setFrame:window_rect display:true animate:true];
}

- (void)mouseDidLeaveImageView:(GBImageView *)view
{
    self.vramStatusLabel.stringValue = @"";
}

- (void)imageView:(GBImageView *)view mouseMovedToX:(NSUInteger)x Y:(NSUInteger)y
{
    if (view == self.tilesetImageView) {
        uint8_t bank = x >= 128? 1 : 0;
        x &= 127;
        uint16_t tile = x / 8 + y / 8 * 16;
        self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10];
    }
    else if (view == self.tilemapImageView) {
        uint16_t map_offset = x / 8 + y / 8 * 32;
        uint16_t map_base = 0x1800;
        GB_map_type_t map_type = (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem;
        GB_tileset_type_t tileset_type = (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem;
        uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC];
        uint8_t *vram = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL);
        
        if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & 0x08)) {
            map_base = 0x1C00;
        }
        
        if (tileset_type == GB_TILESET_AUTO) {
            tileset_type = (lcdc & 0x10)? GB_TILESET_8800 : GB_TILESET_8000;
        }
        
        uint8_t tile = vram[map_base + map_offset];
        uint16_t tile_address = 0;
        if (tileset_type == GB_TILESET_8000) {
            tile_address = 0x8000 + tile * 0x10;
        }
        else {
            tile_address = 0x9000 + (int8_t)tile * 0x10;
        }
        
        if (GB_is_cgb(&gb)) {
            uint8_t attributes = vram[map_base + map_offset + 0x2000];
            self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)",
                                                tile,
                                                attributes & 0x8? 1 : 0,
                                                tile_address,
                                                0x8000 + map_base + map_offset,
                                                (attributes & 0x80) ? 'P' : '-',
                                                (attributes & 0x40) ? 'V' : '-',
                                                (attributes & 0x20) ? 'H' : '-',
                                                attributes & 0x8? 1 : 0,
                                                attributes & 0x7
                                                ];
        }
        else {
            self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x ($%04x) at map address $%04x",
                                                tile,
                                                tile_address,
                                                0x8000 + map_base + map_offset
                                                ];
        }

    }
}

- (GB_oam_info_t *)oamInfo
{
    return _oamInfo;
}

- (IBAction)showVRAMViewer:(id)sender
{
    [self.vramWindow makeKeyAndOrderFront:sender];
    [self reloadVRAMData: nil];
}

- (void) printImage:(uint32_t *)imageBytes height:(unsigned) height
          topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
           exposure:(unsigned) exposure
{
    uint32_t paddedImage[160 * (topMargin + height + bottomMargin)];
    memset(paddedImage, 0xFF, sizeof(paddedImage));
    memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0]));
    if (!self.printerFeedWindow.isVisible) {
        currentPrinterImageData = [[NSMutableData alloc] init];
    }
    [currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)];
    /* UI related code must run on main thread. */
    dispatch_async(dispatch_get_main_queue(), ^{
        self.feedImageView.image = [Document imageFromData:currentPrinterImageData
                                                     width:160
                                                    height:currentPrinterImageData.length / 160 / sizeof(imageBytes[0])
                                                     scale:2.0];
        NSRect frame = self.printerFeedWindow.frame;
        frame.size = self.feedImageView.image.size;
        [self.printerFeedWindow setContentMaxSize:frame.size];
        frame.size.height += self.printerFeedWindow.frame.size.height - self.printerFeedWindow.contentView.frame.size.height;
        [self.printerFeedWindow setFrame:frame display:false animate: self.printerFeedWindow.isVisible];
        [self.printerFeedWindow orderFront:NULL];
    });
    
}

- (void)printDocument:(id)sender
{
    if (self.feedImageView.image.size.height == 0) {
        NSBeep(); return;
    }
    NSImageView *view = [[NSImageView alloc] initWithFrame:(NSRect){{0,0}, self.feedImageView.image.size}];
    view.image = self.feedImageView.image;
    [[NSPrintOperation printOperationWithView:view] runOperationModalForWindow:self.printerFeedWindow delegate:nil didRunSelector:NULL contextInfo:NULL];
}

- (IBAction)savePrinterFeed:(id)sender
{
    bool shouldResume = running;
    [self stop];
    NSSavePanel *savePanel = [NSSavePanel savePanel];
    [savePanel setAllowedFileTypes:@[@"png"]];
    [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) {
        if (result == NSModalResponseOK) {
            [savePanel orderOut:self];
            CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL
                                                                        context:nil
                                                                          hints:nil];
            NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
            [imageRep setSize:(NSSize){160, self.feedImageView.image.size.height / 2}];
            NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
            [data writeToURL:savePanel.URL atomically:false];
            [self.printerFeedWindow setIsVisible:false];
        }
        if (shouldResume) {
            [self start];
        }
    }];
}

- (IBAction)disconnectAllAccessories:(id)sender
{
    [self disconnectLinkCable];
    [self performAtomicBlock:^{
        accessory = GBAccessoryNone;
        GB_disconnect_serial(&gb);
    }];
}

- (IBAction)connectPrinter:(id)sender
{
    [self disconnectLinkCable];
    [self performAtomicBlock:^{
        accessory = GBAccessoryPrinter;
        GB_connect_printer(&gb, printImage);
    }];
}

- (IBAction)connectWorkboy:(id)sender
{
    [self disconnectLinkCable];
    [self performAtomicBlock:^{
        accessory = GBAccessoryWorkboy;
        GB_connect_workboy(&gb, setWorkboyTime, getWorkboyTime);
    }];
}

- (void) updateVolume
{
    _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"];
}

- (void) updateHighpassFilter
{
    if (GB_is_inited(&gb)) {
        GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]);
    }
}

- (void) updateColorCorrectionMode
{
    if (GB_is_inited(&gb)) {
        GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]);
    }
}

- (void) updateLightTemperature
{
    if (GB_is_inited(&gb)) {
        GB_set_light_temperature(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]);
    }
}

- (void) updateInterferenceVolume
{
    if (GB_is_inited(&gb)) {
        GB_set_interference_volume(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]);
    }
}

- (void) updateFrameBlendingMode
{
    self.view.frameBlendingMode = (GB_frame_blending_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"];
}

- (void) updateRewindLength
{
    [self performAtomicBlock:^{
        if (GB_is_inited(&gb)) {
            GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]);
        }
    }];
}

- (void) updateRTCMode
{
    if (GB_is_inited(&gb)) {
        GB_set_rtc_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]);
    }
}

- (void)dmgModelChanged
{
    modelsChanging = true;
    if (current_model == MODEL_DMG) {
        [self reset:nil];
    }
    modelsChanging = false;
}

- (void)sgbModelChanged
{
    modelsChanging = true;
    if (current_model == MODEL_SGB) {
        [self reset:nil];
    }
    modelsChanging = false;
}

- (void)cgbModelChanged
{
    modelsChanging = true;
    if (current_model == MODEL_CGB) {
        [self reset:nil];
    }
    modelsChanging = false;
}

- (void)setFileURL:(NSURL *)fileURL
{
    [super setFileURL:fileURL];
    self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[fileURL path] lastPathComponent]];
    
}

- (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview;
{
    if ([[splitView arrangedSubviews] lastObject] == subview) {
        return true;
    }
    return false;
}

- (CGFloat)splitView:(GBSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex
{
    return 600;
}

- (CGFloat)splitView:(GBSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex 
{
    return splitView.frame.size.width - 321;
}

- (BOOL)splitView:(GBSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view 
{
    if ([[splitView arrangedSubviews] lastObject] == view) {
        return false;
    }
    return true;
}

- (void)splitViewDidResizeSubviews:(NSNotification *)notification
{
    GBSplitView *splitview = notification.object;
    if ([[[splitview arrangedSubviews] firstObject] frame].size.width < 600) {
        [splitview setPosition:600 ofDividerAtIndex:0];
    }
    /* NSSplitView renders its separator without the proper vibrancy, so we made it transparent and move an
       NSBox-based separator that renders properly so it acts like the split view's separator. */
    NSRect rect = self.debuggerVerticalLine.frame;
    rect.origin.x = [[[splitview arrangedSubviews] firstObject] frame].size.width - 1;
    self.debuggerVerticalLine.frame = rect;
}

- (IBAction)showCheats:(id)sender
{
    [self.cheatsWindow makeKeyAndOrderFront:nil];
}

- (IBAction)toggleCheats:(id)sender
{
    GB_set_cheats_enabled(&gb, !GB_cheats_enabled(&gb));
}

- (void)disconnectLinkCable
{
    bool wasRunning = self->running;
    Document *partner = master ?: slave;
    if (partner) {
        wasRunning |= partner->running;
        [self stop];
        partner->master = nil;
        partner->slave = nil;
        master = nil;
        slave = nil;
        if (wasRunning) {
            [partner start];
            [self start];
        }
        GB_set_turbo_mode(&gb, false, false);
        GB_set_turbo_mode(&partner->gb, false, false);
        partner->accessory = GBAccessoryNone;
        accessory = GBAccessoryNone;
    }
}

- (void)connectLinkCable:(NSMenuItem *)sender
{
    [self disconnectAllAccessories:sender];
    Document *partner = [sender representedObject];
    [partner disconnectAllAccessories:sender];
    
    bool wasRunning = self->running;
    [self stop];
    [partner stop];
    GB_set_turbo_mode(&partner->gb, true, true);
    slave = partner;
    partner->master = self;
    linkOffset = 0;
    partner->accessory = GBAccessoryLinkCable;
    accessory = GBAccessoryLinkCable;
    GB_set_serial_transfer_bit_start_callback(&gb, linkCableBitStart);
    GB_set_serial_transfer_bit_start_callback(&partner->gb, linkCableBitStart);
    GB_set_serial_transfer_bit_end_callback(&gb, linkCableBitEnd);
    GB_set_serial_transfer_bit_end_callback(&partner->gb, linkCableBitEnd);
    if (wasRunning) {
        [self start];
    }
}

- (void)linkCableBitStart:(bool)bit
{
    linkCableBit = bit;
}

-(bool)linkCableBitEnd
{
    bool ret = GB_serial_get_data_bit(&self.partner->gb);
    GB_serial_set_data_bit(&self.partner->gb, linkCableBit);
    return ret;
}

- (void)infraredStateChanged:(bool)state
{
    if (self.partner) {
        GB_set_infrared_input(&self.partner->gb, state);
    }
}

-(Document *)partner
{
    return slave ?: master;
}

- (bool)isSlave
{
    return master;
}

- (GB_gameboy_t *)gb
{
    return &gb;
}

- (NSImage *)takeScreenshot
{
    NSImage *ret = nil;
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]) {
        ret = [_view renderToImage];
        [ret lockFocus];
        NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0,
                                                                                                   ret.size.width, ret.size.height)];
        [ret unlockFocus];
        ret = [[NSImage alloc] initWithSize:ret.size];
        [ret addRepresentation:bitmapRep];
    }
    if (!ret) {
        ret = [Document imageFromData:[NSData dataWithBytesNoCopy:_view.currentBuffer
                                                           length:GB_get_screen_width(&gb) * GB_get_screen_height(&gb) * 4
                                                     freeWhenDone:false]
                                width:GB_get_screen_width(&gb)
                               height:GB_get_screen_height(&gb)
                                scale:1.0];
    }
    return ret;
}

- (NSString *)screenshotFilename
{
    NSDate *date = [NSDate date];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateStyle = NSDateFormatterLongStyle;
    dateFormatter.timeStyle = NSDateFormatterMediumStyle;
    return [[NSString stringWithFormat:@"%@ – %@.png",
             self.fileURL.lastPathComponent.stringByDeletingPathExtension,
             [dateFormatter stringFromDate:date]] stringByReplacingOccurrencesOfString:@":" withString:@"."]; // Gotta love Mac OS Classic

}

- (IBAction)saveScreenshot:(id)sender
{
    NSString *folder = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBScreenshotFolder"];
    BOOL isDirectory = false;
    if (folder) {
        [[NSFileManager defaultManager] fileExistsAtPath:folder isDirectory:&isDirectory];
    }
    if (!folder) {
        bool shouldResume = running;
        [self stop];
        NSOpenPanel *openPanel = [NSOpenPanel openPanel];
        openPanel.canChooseFiles = false;
        openPanel.canChooseDirectories = true;
        openPanel.message = @"Choose a folder for screenshots";
        [openPanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
            if (result == NSModalResponseOK) {
                [[NSUserDefaults standardUserDefaults] setObject:openPanel.URL.path
                                                          forKey:@"GBScreenshotFolder"];
                [self saveScreenshot:sender];
            }
            if (shouldResume) {
                [self start];
            }
            
        }];
        return;
    }
    NSImage *image = [self takeScreenshot];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateStyle = NSDateFormatterLongStyle;
    dateFormatter.timeStyle = NSDateFormatterMediumStyle;
    NSString *filename = [self screenshotFilename];
    filename = [folder stringByAppendingPathComponent:filename];
    unsigned i = 2;
    while ([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
        filename = [[filename stringByDeletingPathExtension] stringByAppendingFormat:@" %d.png", i++];
    }
    
    NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject;
    NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
    [data writeToFile:filename atomically:false];
    [self.osdView displayText:@"Screenshot saved"];
}

- (IBAction)saveScreenshotAs:(id)sender
{
    bool shouldResume = running;
    [self stop];
    NSImage *image = [self takeScreenshot];
    NSSavePanel *savePanel = [NSSavePanel savePanel];
    [savePanel setNameFieldStringValue:[self screenshotFilename]];
    [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
        if (result == NSModalResponseOK) {
            [savePanel orderOut:self];
            NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject;
            NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
            [data writeToURL:savePanel.URL atomically:false];
            [[NSUserDefaults standardUserDefaults] setObject:savePanel.URL.path.stringByDeletingLastPathComponent
                                                      forKey:@"GBScreenshotFolder"];
        }
        if (shouldResume) {
            [self start];
        }
    }];
    [self.osdView displayText:@"Screenshot saved"];
}

- (IBAction)copyScreenshot:(id)sender
{
    NSImage *image = [self takeScreenshot];
    [[NSPasteboard generalPasteboard] clearContents];
    [[NSPasteboard generalPasteboard] writeObjects:@[image]];
    [self.osdView displayText:@"Screenshot copied"];
}

- (IBAction)toggleDisplayBackground:(id)sender
{
    GB_set_background_rendering_disabled(&gb, !GB_is_background_rendering_disabled(&gb));
}

- (IBAction)toggleDisplayObjects:(id)sender
{
    GB_set_object_rendering_disabled(&gb, !GB_is_object_rendering_disabled(&gb));
}

- (IBAction)newCartridgeInstance:(id)sender
{
    bool shouldResume = running;
    [self stop];
    NSSavePanel *savePanel = [NSSavePanel savePanel];
    [savePanel setAllowedFileTypes:@[@"gbcart"]];
    [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
        if (result == NSModalResponseOK) {
            [savePanel orderOut:self];
            NSString *romPath = self.romPath;
            [[NSFileManager defaultManager] trashItemAtURL:savePanel.URL resultingItemURL:nil error:nil];
            [[NSFileManager defaultManager] createDirectoryAtURL:savePanel.URL withIntermediateDirectories:false attributes:nil error:nil];
            [[NSString stringWithFormat:@"%@\n%@\n%@",
              [romPath pathRelativeToDirectory:savePanel.URL.path],
              romPath,
              [[NSURL fileURLWithPath:romPath].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")]
            ] writeToURL:[savePanel.URL URLByAppendingPathComponent:@"rom.gbl"] atomically:false encoding:NSUTF8StringEncoding error:nil];
            [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:savePanel.URL display:true completionHandler:nil];
        }
        if (shouldResume) {
            [self start];
        }
    }];
}

- (IBAction)toggleAudioRecording:(id)sender
{

    bool shouldResume = running;
    [self stop];
    if (_isRecordingAudio) {
        _isRecordingAudio = false;
        int error = GB_stop_audio_recording(&gb);
        if (error) {
            NSAlert *alert = [[NSAlert alloc] init];
            [alert setMessageText:[NSString stringWithFormat:@"Could not finalize recording: %s", strerror(error)]];
            [alert setAlertStyle:NSAlertStyleCritical];
            [alert runModal];
        }
        else {
            [self.osdView displayText:@"Audio recording ended"];
        }
        if (shouldResume) {
            [self start];
        }
        return;
    }
    _audioSavePanel = [NSSavePanel savePanel];
    if (!self.audioRecordingAccessoryView) {
        [[NSBundle mainBundle] loadNibNamed:@"AudioRecordingAccessoryView" owner:self topLevelObjects:nil];
    }
    _audioSavePanel.accessoryView = self.audioRecordingAccessoryView;
    [self audioFormatChanged:self.audioFormatButton];
    
    [_audioSavePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
        if (result == NSModalResponseOK) {
            [_audioSavePanel orderOut:self];
            int error = GB_start_audio_recording(&gb, _audioSavePanel.URL.fileSystemRepresentation, self.audioFormatButton.selectedTag);
            if (error) {
                NSAlert *alert = [[NSAlert alloc] init];
                [alert setMessageText:[NSString stringWithFormat:@"Could not start recording: %s", strerror(error)]];
                [alert setAlertStyle:NSAlertStyleCritical];
                [alert runModal];
            }
            else {
                [self.osdView displayText:@"Audio recording started"];
                _isRecordingAudio = true;
            }
        }
        if (shouldResume) {
            [self start];
        }
        _audioSavePanel = nil;
    }];
}

- (IBAction)audioFormatChanged:(NSPopUpButton *)sender
{
    switch ((GB_audio_format_t)sender.selectedTag) {
        case GB_AUDIO_FORMAT_RAW:
            _audioSavePanel.allowedFileTypes = @[@"raw", @"pcm"];
            break;
        case GB_AUDIO_FORMAT_AIFF:
            _audioSavePanel.allowedFileTypes = @[@"aiff", @"aif", @"aifc"];
            break;
        case GB_AUDIO_FORMAT_WAV:
            _audioSavePanel.allowedFileTypes = @[@"wav"];
            break;
    }
}

@end
