#import "AppDelegate.h"
#include "GBButtons.h"
#include "GBView.h"
#include <Core/gb.h>
#import <Carbon/Carbon.h>
#import <JoyKit/JoyKit.h>
#import <WebKit/WebKit.h>

#define UPDATE_SERVER "https://sameboy.github.io"

static uint32_t color_to_int(NSColor *color)
{
    color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
    return (((unsigned)(color.redComponent * 0xFF)) << 16) |
           (((unsigned)(color.greenComponent * 0xFF)) << 8) |
           ((unsigned)(color.blueComponent * 0xFF));
}

@implementation AppDelegate
{
    NSWindow *preferences_window;
    NSArray<NSView *> *preferences_tabs;
    NSString *_lastVersion;
    NSString *_updateURL;
    NSURLSessionDownloadTask *_updateTask;
    enum {
        UPDATE_DOWNLOADING,
        UPDATE_EXTRACTING,
        UPDATE_WAIT_INSTALL,
        UPDATE_INSTALLING,
        UPDATE_FAILED,
    } _updateState;
    NSString *_downloadDirectory;
}

- (void) applicationDidFinishLaunching:(NSNotification *)notification
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    for (unsigned i = 0; i < GBButtonCount; i++) {
        if ([[defaults objectForKey:button_to_preference_name(i, 0)] isKindOfClass:[NSString class]]) {
            [defaults removeObjectForKey:button_to_preference_name(i, 0)];
        }
    }
    [[NSUserDefaults standardUserDefaults] registerDefaults:@{
                                                              @"GBRight": @(kVK_RightArrow),
                                                              @"GBLeft": @(kVK_LeftArrow),
                                                              @"GBUp": @(kVK_UpArrow),
                                                              @"GBDown": @(kVK_DownArrow),

                                                              @"GBA": @(kVK_ANSI_X),
                                                              @"GBB": @(kVK_ANSI_Z),
                                                              @"GBSelect": @(kVK_Delete),
                                                              @"GBStart": @(kVK_Return),

                                                              @"GBTurbo": @(kVK_Space),
                                                              @"GBRewind": @(kVK_Tab),
                                                              @"GBSlow-Motion": @(kVK_Shift),

                                                              @"GBFilter": @"NearestNeighbor",
                                                              @"GBColorCorrection": @(GB_COLOR_CORRECTION_EMULATE_HARDWARE),
                                                              @"GBHighpassFilter": @(GB_HIGHPASS_REMOVE_DC_OFFSET),
                                                              @"GBRewindLength": @(10),
                                                              @"GBFrameBlendingMode": @([defaults boolForKey:@"DisableFrameBlending"]? GB_FRAME_BLENDING_MODE_DISABLED : GB_FRAME_BLENDING_MODE_ACCURATE),
                                                              
                                                              @"GBDMGModel": @(GB_MODEL_DMG_B),
                                                              @"GBCGBModel": @(GB_MODEL_CGB_E),
                                                              @"GBSGBModel": @(GB_MODEL_SGB2),
                                                              @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY),
                                                              
                                                              @"GBVolume": @(1.0),
                                                              
                                                              @"GBMBC7JoystickOverride": @NO,
                                                              @"GBMBC7AllowMouse": @YES,
                                                              }];
    
    [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{
        JOYAxes2DEmulateButtonsKey: @YES,
        JOYHatsEmulateButtonsKey: @YES,
    }];
    
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
        [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self;
    }
    
    [self askAutoUpdates];
    
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]) {
        [self checkForUpdates];
    }
    
    if ([[NSProcessInfo processInfo].arguments containsObject:@"--update-launch"]) {
        [NSApp activateIgnoringOtherApps:true];
    }
}

- (IBAction)toggleDeveloperMode:(id)sender
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setBool:![defaults boolForKey:@"DeveloperMode"] forKey:@"DeveloperMode"];
}

- (IBAction)switchPreferencesTab:(id)sender
{
    for (NSView *view in preferences_tabs) {
        [view removeFromSuperview];
    }
    NSView *tab = preferences_tabs[[sender tag]];
    NSRect old = [_preferencesWindow frame];
    NSRect new = [_preferencesWindow frameRectForContentRect:tab.frame];
    new.origin.x = old.origin.x;
    new.origin.y = old.origin.y + (old.size.height - new.size.height);
    [_preferencesWindow setFrame:new display:true animate:_preferencesWindow.visible];
    [_preferencesWindow.contentView addSubview:tab];
}

- (BOOL)validateMenuItem:(NSMenuItem *)anItem
{
    if ([anItem action] == @selector(toggleDeveloperMode:)) {
        [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]];
    }
    
    if (anItem == self.linkCableMenuItem) {
        return [[NSDocumentController sharedDocumentController] documents].count > 1;
    }
    return true;
}

- (void)menuNeedsUpdate:(NSMenu *)menu
{
    NSMutableArray *items = [NSMutableArray array];
    NSDocument *currentDocument = [[NSDocumentController sharedDocumentController] currentDocument];
    
    for (NSDocument *document in [[NSDocumentController sharedDocumentController] documents]) {
        if (document == currentDocument) continue;
        NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:document.displayName action:@selector(connectLinkCable:) keyEquivalent:@""];
        item.representedObject = document;
        item.image = [[NSWorkspace sharedWorkspace] iconForFile:document.fileURL.path];
        [item.image setSize:NSMakeSize(16, 16)];
        [items addObject:item];
    }
    menu.itemArray = items;
}

- (IBAction) showPreferences: (id) sender
{
    NSArray *objects;
    if (!_preferencesWindow) {
        [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects];
        NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject];
        _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier];
        preferences_tabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab, self.updatesTab];
        [self switchPreferencesTab:first_toolbar_item];
        [_preferencesWindow center];
#ifndef UPDATE_SUPPORT
        [_preferencesWindow.toolbar removeItemAtIndex:4];
#endif
    }
    [_preferencesWindow makeKeyAndOrderFront:self];
}

- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
{
    [self askAutoUpdates];
    /* Bring an existing panel to the foreground */
    for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
        if ([window isKindOfClass:[NSOpenPanel class]]) {
            [(NSOpenPanel *)window makeKeyAndOrderFront:nil];
            return true;
        }
    }
    [[NSDocumentController sharedDocumentController] openDocument:self];
    return true;
}

- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
{
    [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:true];
}

- (void)updateFound
{
    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/raw_changes"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        
        NSColor *linkColor = [NSColor colorWithRed:0.125 green:0.325 blue:1.0 alpha:1.0];
        if (@available(macOS 10.10, *)) {
            linkColor = [NSColor linkColor];
        }
        
        NSString *changes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSRange cutoffRange = [changes rangeOfString:@"<!--(" GB_VERSION ")-->"];
        if (cutoffRange.location != NSNotFound) {
            changes = [changes substringToIndex:cutoffRange.location];
        }
        
        NSString *html = [NSString stringWithFormat:@"<!DOCTYPE html><html><head><title></title>"
                          "<style>html {background-color:transparent; color: #%06x; line-height:1.5} a:link, a:visited{color:#%06x; text-decoration:none}</style>"
                          "</head><body>%@</body></html>",
                          color_to_int([NSColor textColor]),
                          color_to_int(linkColor),
                          changes];
        
        if ([(NSHTTPURLResponse *)response statusCode] == 200) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSArray *objects;
                [[NSBundle mainBundle] loadNibNamed:@"UpdateWindow" owner:self topLevelObjects:&objects];
                self.updateChanges.preferences.standardFontFamily = [NSFont systemFontOfSize:0].familyName;
                self.updateChanges.preferences.fixedFontFamily = @"Menlo";
                self.updateChanges.drawsBackground = false;
                [self.updateChanges.mainFrame loadHTMLString:html baseURL:nil];
            });
        }
    }] resume];
}

- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
{
    // Disable reload context menu
    if ([defaultMenuItems count] <= 2) {
        return nil;
    }
    return defaultMenuItems;
}

- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sender.mainFrame.frameView.documentView.enclosingScrollView.drawsBackground = true;
        sender.mainFrame.frameView.documentView.enclosingScrollView.backgroundColor = [NSColor textBackgroundColor];
        sender.policyDelegate = self;
        [self.updateWindow center];
        [self.updateWindow makeKeyAndOrderFront:nil];
    });
}

- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener
{
    [listener ignore];
    [[NSWorkspace sharedWorkspace] openURL:[request URL]];
}

- (void)checkForUpdates
{
#ifdef UPDATE_SUPPORT
    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/latest_version"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.updatesSpinner stopAnimation:nil];
            [self.updatesButton setEnabled:true];
        });
        if ([(NSHTTPURLResponse *)response statusCode] == 200) {
            NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSArray <NSString *> *components = [string componentsSeparatedByString:@"|"];
            if (components.count != 2) return;
            _lastVersion = components[0];
            _updateURL = components[1];
            if (![@GB_VERSION isEqualToString:_lastVersion] &&
                ![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBSkippedVersion"] isEqualToString:_lastVersion]) {
                [self updateFound];
            }
        }
    }] resume];
#endif
}

- (IBAction)userCheckForUpdates:(id)sender
{
    if (self.updateWindow) {
        [self.updateWindow makeKeyAndOrderFront:sender];
    }
    else {
        [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"GBSkippedVersion"];
        [self checkForUpdates];
        [sender setEnabled:false];
        [self.updatesSpinner startAnimation:sender];
    }
}

- (void)askAutoUpdates
{
#ifdef UPDATE_SUPPORT
    if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAskedAutoUpdates"]) {
        NSAlert *alert = [[NSAlert alloc] init];
        alert.messageText = @"Should SameBoy check for updates when launched?";
        alert.informativeText = @"SameBoy is frequently updated with new features, accuracy improvements, and bug fixes. This setting can always be changed in the preferences window.";
        [alert addButtonWithTitle:@"Check on Launch"];
        [alert addButtonWithTitle:@"Don't Check on Launch"];
        
        [[NSUserDefaults standardUserDefaults] setBool:[alert runModal] == NSAlertFirstButtonReturn forKey:@"GBAutoUpdatesEnabled"];
        [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBAskedAutoUpdates"];
    }
#endif
}

- (IBAction)skipVersion:(id)sender
{
    [[NSUserDefaults standardUserDefaults] setObject:_lastVersion forKey:@"GBSkippedVersion"];
    [self.updateWindow performClose:sender];
}

- (IBAction)installUpdate:(id)sender
{
    [self.updateProgressSpinner startAnimation:nil];
    self.updateProgressButton.title = @"Cancel";
    self.updateProgressButton.enabled = true;
    self.updateProgressLabel.stringValue = @"Downloading update...";
    _updateState = UPDATE_DOWNLOADING;
    _updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        _updateTask = nil;
        dispatch_sync(dispatch_get_main_queue(), ^{
            self.updateProgressButton.enabled = false;
            self.updateProgressLabel.stringValue = @"Extracting update...";
            _updateState = UPDATE_EXTRACTING;
        });
        
        _downloadDirectory = [[[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory
                                                                     inDomain:NSUserDomainMask
                                                            appropriateForURL:[[NSBundle mainBundle] bundleURL]
                                                                       create:true
                                                                        error:nil] path];
        NSTask *unzipTask;
        if (!_downloadDirectory) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                self.updateProgressButton.enabled = false;
                self.updateProgressLabel.stringValue = @"Failed to extract update.";
                _updateState = UPDATE_FAILED;
                self.updateProgressButton.title = @"Close";
                self.updateProgressButton.enabled = true;
                [self.updateProgressSpinner stopAnimation:nil];
            });
            return;
        }
        
        unzipTask = [[NSTask alloc] init];
        unzipTask.launchPath = @"/usr/bin/unzip";
        unzipTask.arguments = @[location.path, @"-d", _downloadDirectory];
        [unzipTask launch];
        [unzipTask waitUntilExit];
        if (unzipTask.terminationStatus != 0 || unzipTask.terminationReason != NSTaskTerminationReasonExit) {
            [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
            dispatch_sync(dispatch_get_main_queue(), ^{
                self.updateProgressButton.enabled = false;
                self.updateProgressLabel.stringValue = @"Failed to extract update.";
                _updateState = UPDATE_FAILED;
                self.updateProgressButton.title = @"Close";
                self.updateProgressButton.enabled = true;
                [self.updateProgressSpinner stopAnimation:nil];
            });
            return;
        }
        
        dispatch_sync(dispatch_get_main_queue(), ^{
            self.updateProgressButton.enabled = false;
            self.updateProgressLabel.stringValue = @"Update ready, save your game progress and click Install.";
            _updateState = UPDATE_WAIT_INSTALL;
            self.updateProgressButton.title = @"Install";
            self.updateProgressButton.enabled = true;
            [self.updateProgressSpinner stopAnimation:nil];
        });
    }];
    [_updateTask resume];
    
    self.updateProgressWindow.preventsApplicationTerminationWhenModal = false;
    [self.updateWindow beginSheet:self.updateProgressWindow completionHandler:^(NSModalResponse returnCode) {
        [self.updateWindow close];
    }];
}

- (void)performUpgrade
{
    self.updateProgressButton.enabled = false;
    self.updateProgressLabel.stringValue = @"Instaling update...";
    _updateState = UPDATE_INSTALLING;
    self.updateProgressButton.enabled = false;
    [self.updateProgressSpinner startAnimation:nil];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSString *executablePath = [[NSBundle mainBundle] executablePath];
        NSString *contentsPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"];
        NSString *contentsTempPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"TempContents"];
        NSString *updateContentsPath = [_downloadDirectory stringByAppendingPathComponent:@"SameBoy.app/Contents"];
        NSError *error = nil;
        [[NSFileManager defaultManager] moveItemAtPath:contentsPath toPath:contentsTempPath error:&error];
        if (error) {
            [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
            _downloadDirectory = nil;
            dispatch_sync(dispatch_get_main_queue(), ^{
                self.updateProgressButton.enabled = false;
                self.updateProgressLabel.stringValue = @"Failed to install update.";
                _updateState = UPDATE_FAILED;
                self.updateProgressButton.title = @"Close";
                self.updateProgressButton.enabled = true;
                [self.updateProgressSpinner stopAnimation:nil];
            });
            return;
        }
        [[NSFileManager defaultManager] moveItemAtPath:updateContentsPath toPath:contentsPath error:&error];
        if (error) {
            [[NSFileManager defaultManager] moveItemAtPath:contentsTempPath toPath:contentsPath error:nil];
            [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
            _downloadDirectory = nil;
            dispatch_sync(dispatch_get_main_queue(), ^{
                self.updateProgressButton.enabled = false;
                self.updateProgressLabel.stringValue = @"Failed to install update.";
                _updateState = UPDATE_FAILED;
                self.updateProgressButton.title = @"Close";
                self.updateProgressButton.enabled = true;
                [self.updateProgressSpinner stopAnimation:nil];
            });
            return;
        }
        [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
        [[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil];
        _downloadDirectory = nil;
        atexit_b(^{
            execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL);
        });
        
        dispatch_async(dispatch_get_main_queue(), ^{
            [NSApp terminate:nil];
        });
    });
}

- (IBAction)updateAction:(id)sender
{
    switch (_updateState) {
        case UPDATE_DOWNLOADING:
            [_updateTask cancelByProducingResumeData:nil];
            _updateTask = nil;
            [self.updateProgressWindow close];
            break;
        case UPDATE_WAIT_INSTALL:
            [self performUpgrade];
            break;
        case UPDATE_EXTRACTING:
        case UPDATE_INSTALLING:
            break;
        case UPDATE_FAILED:
            [self.updateProgressWindow close];
            break;
    }
}

- (void)dealloc
{
    if (_downloadDirectory) {
        [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
    }
}

- (IBAction)nop:(id)sender
{
}
@end