//========================================================================
// GLFW 3.4 macOS - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2009-2019 Camilla Löwy <elmindreda@glfw.org>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//    claim that you wrote the original software. If you use this software
//    in a product, an acknowledgment in the product documentation would
//    be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
//    be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
//    distribution.
//
//========================================================================
// It is fine to use C99 in this file because it will not be built with VS
//========================================================================

#include "internal.h"
#include "../kitty/monotonic.h"
#include <sys/param.h> // For MAXPATHLEN
#include <sys/sysctl.h>
#include <pthread.h>

// Needed for _NSGetProgname
#include <crt_externs.h>

// Change to our application bundle's resources directory, if present
//
static void changeToResourcesDirectory(void)
{
    char resourcesPath[MAXPATHLEN];

    CFBundleRef bundle = CFBundleGetMainBundle();
    if (!bundle)
        return;

    CFURLRef resourcesURL = CFBundleCopyResourcesDirectoryURL(bundle);

    CFStringRef last = CFURLCopyLastPathComponent(resourcesURL);
    if (CFStringCompare(CFSTR("Resources"), last, 0) != kCFCompareEqualTo)
    {
        CFRelease(last);
        CFRelease(resourcesURL);
        return;
    }

    CFRelease(last);

    if (!CFURLGetFileSystemRepresentation(resourcesURL,
                                          true,
                                          (UInt8*) resourcesPath,
                                          MAXPATHLEN))
    {
        CFRelease(resourcesURL);
        return;
    }

    CFRelease(resourcesURL);

    chdir(resourcesPath);
}

// Set up the menu bar (manually)
// This is nasty, nasty stuff -- calls to undocumented semi-private APIs that
// could go away at any moment, lots of stuff that really should be
// localize(d|able), etc.  Add a nib to save us this horror.
//
static void createMenuBar(void)
{
    size_t i;
    NSString* appName = nil;
    NSDictionary* bundleInfo = [[NSBundle mainBundle] infoDictionary];
    NSString* nameKeys[] =
    {
        @"CFBundleDisplayName",
        @"CFBundleName",
        @"CFBundleExecutable",
    };

    // Try to figure out what the calling application is called

    for (i = 0;  i < sizeof(nameKeys) / sizeof(nameKeys[0]);  i++)
    {
        id name = bundleInfo[nameKeys[i]];
        if (name &&
            [name isKindOfClass:[NSString class]] &&
            ![name isEqualToString:@""])
        {
            appName = name;
            break;
        }
    }

    if (!appName)
    {
        char** progname = _NSGetProgname();
        if (progname && *progname)
            appName = @(*progname);
        else
            appName = @"GLFW Application";
    }

    NSMenu* bar = [[NSMenu alloc] init];
    [NSApp setMainMenu:bar];

    NSMenuItem* appMenuItem =
        [bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
    NSMenu* appMenu = [[NSMenu alloc] init];
    [appMenuItem setSubmenu:appMenu];

    [appMenu addItemWithTitle:[NSString stringWithFormat:@"About %@", appName]
                       action:@selector(orderFrontStandardAboutPanel:)
                keyEquivalent:@""];
    [appMenu addItem:[NSMenuItem separatorItem]];
    NSMenu* servicesMenu = [[NSMenu alloc] init];
    [NSApp setServicesMenu:servicesMenu];
    [[appMenu addItemWithTitle:@"Services"
                       action:NULL
                keyEquivalent:@""] setSubmenu:servicesMenu];
    [servicesMenu release];
    [appMenu addItem:[NSMenuItem separatorItem]];
    [appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", appName]
                       action:@selector(hide:)
                keyEquivalent:@"h"];
    [[appMenu addItemWithTitle:@"Hide Others"
                       action:@selector(hideOtherApplications:)
                keyEquivalent:@"h"]
        setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand];
    [appMenu addItemWithTitle:@"Show All"
                       action:@selector(unhideAllApplications:)
                keyEquivalent:@""];
    [appMenu addItem:[NSMenuItem separatorItem]];
    [appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", appName]
                       action:@selector(terminate:)
                keyEquivalent:@"q"];

    NSMenuItem* windowMenuItem =
        [bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
    [bar release];
    NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
    [NSApp setWindowsMenu:windowMenu];
    [windowMenuItem setSubmenu:windowMenu];

    [windowMenu addItemWithTitle:@"Minimize"
                          action:@selector(performMiniaturize:)
                   keyEquivalent:@"m"];
    [windowMenu addItemWithTitle:@"Zoom"
                          action:@selector(performZoom:)
                   keyEquivalent:@""];
    [windowMenu addItem:[NSMenuItem separatorItem]];
    [windowMenu addItemWithTitle:@"Bring All to Front"
                          action:@selector(arrangeInFront:)
                   keyEquivalent:@""];

    // TODO: Make this appear at the bottom of the menu (for consistency)
    [windowMenu addItem:[NSMenuItem separatorItem]];
    [[windowMenu addItemWithTitle:@"Enter Full Screen"
                           action:@selector(toggleFullScreen:)
                    keyEquivalent:@"f"]
     setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];

    // Prior to Snow Leopard, we need to use this oddly-named semi-private API
    // to get the application menu working properly.
    SEL setAppleMenuSelector = NSSelectorFromString(@"setAppleMenu:");
    [NSApp performSelector:setAppleMenuSelector withObject:appMenu];
}

// Retrieve Unicode data for the current keyboard layout
//
static bool updateUnicodeDataNS(void)
{
    if (_glfw.ns.inputSource)
    {
        CFRelease(_glfw.ns.inputSource);
        _glfw.ns.inputSource = NULL;
        _glfw.ns.unicodeData = nil;
    }

    for (_GLFWwindow *window = _glfw.windowListHead;  window;  window = window->next)
        window->ns.deadKeyState = 0;

    _glfw.ns.inputSource = TISCopyCurrentKeyboardLayoutInputSource();
    if (!_glfw.ns.inputSource)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to retrieve keyboard layout input source");
        return false;
    }

    _glfw.ns.unicodeData =
        TISGetInputSourceProperty(_glfw.ns.inputSource,
                                  kTISPropertyUnicodeKeyLayoutData);
    if (!_glfw.ns.unicodeData)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to retrieve keyboard layout Unicode data");
        return false;
    }

    return true;
}

// Load HIToolbox.framework and the TIS symbols we need from it
//
static bool initializeTIS(void)
{
    // This works only because Cocoa has already loaded it properly
    _glfw.ns.tis.bundle =
        CFBundleGetBundleWithIdentifier(CFSTR("com.apple.HIToolbox"));
    if (!_glfw.ns.tis.bundle)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to load HIToolbox.framework");
        return false;
    }

    CFStringRef* kPropertyUnicodeKeyLayoutData =
        CFBundleGetDataPointerForName(_glfw.ns.tis.bundle,
                                      CFSTR("kTISPropertyUnicodeKeyLayoutData"));
    *(void **)&_glfw.ns.tis.CopyCurrentKeyboardLayoutInputSource =
        CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle,
                                          CFSTR("TISCopyCurrentKeyboardLayoutInputSource"));
    *(void **)&_glfw.ns.tis.GetInputSourceProperty =
        CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle,
                                          CFSTR("TISGetInputSourceProperty"));
    *(void **)&_glfw.ns.tis.GetKbdType =
        CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle,
                                          CFSTR("LMGetKbdType"));

    if (!kPropertyUnicodeKeyLayoutData ||
        !TISCopyCurrentKeyboardLayoutInputSource ||
        !TISGetInputSourceProperty ||
        !LMGetKbdType)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to load TIS API symbols");
        return false;
    }

    _glfw.ns.tis.kPropertyUnicodeKeyLayoutData =
        *kPropertyUnicodeKeyLayoutData;

    return updateUnicodeDataNS();
}

static void
display_reconfigured(CGDirectDisplayID display UNUSED, CGDisplayChangeSummaryFlags flags, void *userInfo UNUSED)
{
    if (flags & kCGDisplayBeginConfigurationFlag) {
        return;
    }
    if (flags & kCGDisplaySetModeFlag) {
        // GPU possibly changed
    }
}

static NSDictionary<NSString*,NSNumber*> *global_shortcuts = nil;

@interface GLFWHelper : NSObject
@end

@implementation GLFWHelper

- (void)selectedKeyboardInputSourceChanged:(NSObject* )object
{
    (void)object;
    updateUnicodeDataNS();
}

- (void)doNothing:(id)object
{
    (void)object;
}

// watch for settings change and rebuild global_shortcuts using key/value observing on NSUserDefaults
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    (void)keyPath; (void)object; (void)change; (void)context;
    if (global_shortcuts != nil) {
        [global_shortcuts release];
        global_shortcuts = nil;
    }
}

@end // GLFWHelper

// Delegate for application related notifications {{{

@interface GLFWApplicationDelegate : NSObject <NSApplicationDelegate>
    - (void)handleAppearanceChange;
@end

@implementation GLFWApplicationDelegate

- (void)applicationDidActivate:(NSNotification *)notification {
    NSRunningApplication *app = notification.userInfo[NSWorkspaceApplicationKey];
    if (app && app.processIdentifier != getpid()) {
        _glfw.ns.previous_front_most_application = app.processIdentifier;
        debug_rendering("Front most application changed to: %s pid: %d\n", app.bundleIdentifier.UTF8String, app.processIdentifier)
    }
}

- (void)applicationDidBecomeActive:(NSNotification *)notification {
    (void)notification;
    // When the application becomes active after switching spaces (e.g., swiping
    // back from a fullscreen app on another space), macOS may not send
    // windowDidBecomeKey: for the already-key window. This leaves GLFW thinking
    // no window has focus (since windowDidResignKey: was sent when leaving).
    // Ensure GLFW's focus state is updated to match the actual key window.
    NSWindow *keyWindow = [NSApp keyWindow];
    if (keyWindow && !_glfw.focusedWindowId) {
        for (_GLFWwindow *window = _glfw.windowListHead; window; window = window->next) {
            if (window->ns.object == keyWindow) {
                if (_glfw.focusedWindowId != window->id) _glfwInputWindowFocus(window, true);
                break;
            }
        }
    }
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
    (void)sender;
    if (_glfw.callbacks.application_close) _glfw.callbacks.application_close(0);
    return NSTerminateCancel;
}

- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
    return YES;
}

static GLFWapplicationshouldhandlereopenfun handle_reopen_callback = NULL;

- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag
{
    (void)sender;
    if (!handle_reopen_callback) return YES;
    if (handle_reopen_callback(flag)) return YES;
    return NO;
}

- (void)applicationDidChangeScreenParameters:(NSNotification *) notification
{
    (void)notification;
    _GLFWwindow* window;

    for (window = _glfw.windowListHead;  window;  window = window->next)
    {
        if (window->context.client != GLFW_NO_API)
            [window->context.nsgl.object update];
    }

    _glfwPollMonitorsNS();
}

static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;

- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
    (void)notification;
    if (_glfw.hints.init.ns.menubar)
    {
        // In case we are unbundled, make us a proper UI application
        [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];

        // Menu bar setup must go between sharedApplication and finishLaunching
        // in order to properly emulate the behavior of NSApplicationMain

        if ([[NSBundle mainBundle] pathForResource:@"MainMenu" ofType:@"nib"])
        {
            [[NSBundle mainBundle] loadNibNamed:@"MainMenu"
                                          owner:NSApp
                                topLevelObjects:&_glfw.ns.nibObjects];
        }
        else
            createMenuBar();
    }
    if (finish_launching_callback) finish_launching_callback(false);
}

- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
    (void)sender;
    if (!filename || !_glfw.ns.url_open_callback) return NO;
    const char *url = NULL;
    @try {
        url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String];
    } @catch(NSException *exc) {
        NSLog(@"Converting openFile filename: %@ failed with error: %@", filename, exc.reason);
        return NO;
    }
    if (!url) return NO;
    return _glfw.ns.url_open_callback(url);
}

- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames {
    (void)sender;
    if (!_glfw.ns.url_open_callback || !filenames) return;
    for (id x in filenames) {
        NSString *filename = x;
        const char *url = NULL;
        @try {
            url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String];
        } @catch(NSException *exc) {
            NSLog(@"Converting openFiles filename: %@ failed with error: %@", filename, exc.reason);
        }
        if (url) _glfw.ns.url_open_callback(url);
    }
}

// Remove openFile and openFiles when the minimum supported macOS version is 10.13
- (void)application:(NSApplication *)sender openURLs:(NSArray<NSURL *> *)urls
{
    (void)sender;
    if (!_glfw.ns.url_open_callback || !urls) return;
    for (id x in urls) {
        NSURL *ns_url = x;
        const char *url = NULL;
        @try {
            url = [[ns_url absoluteString] UTF8String];
        } @catch(NSException *exc) {
            NSLog(@"Converting openURLs url: %@ failed with error: %@", ns_url, exc.reason);
        }
        if (url) _glfw.ns.url_open_callback(url);
    }
}

static void *AppearanceObservationContext = &AppearanceObservationContext;
static NSDate *application_finished_launching_at = nil;

- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
    (void)notification;
    [[NSApplication sharedApplication] addObserver:self
        forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
        context:AppearanceObservationContext];

    if (finish_launching_callback) finish_launching_callback(true);
    [NSApp stop:nil];

    CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL);
    _glfwCocoaPostEmptyEvent();
    application_finished_launching_at = [NSDate date];
}

GLFWAPI GLFWColorScheme glfwGetCurrentSystemColorTheme(bool query_if_unintialized) {
    (void)query_if_unintialized;
    int theme_type = GLFW_COLOR_SCHEME_NO_PREFERENCE;
    NSAppearance *changedAppearance = NSApp.effectiveAppearance;
    NSAppearanceName newAppearance = [changedAppearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]];
    if([newAppearance isEqualToString:NSAppearanceNameDarkAqua]){
        theme_type = GLFW_COLOR_SCHEME_DARK;
    } else {
        theme_type = GLFW_COLOR_SCHEME_LIGHT;
    }
    return theme_type;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context {
                           if (context == AppearanceObservationContext) {
        if ([keyPath isEqualToString:@"effectiveAppearance"]) {
            // The initial call (from NSKeyValueObservingOptionInitial) might happen on a background thread.
            // Dispatch to the main thread to be safe, especially if updating UI.
            __block __typeof__(self) weakSelf = self;
            dispatch_async(dispatch_get_main_queue(), ^{
                [weakSelf handleAppearanceChange];
            });
        }
    } else {
        // If the context doesn't match, pass the notification to the superclass.
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)handleAppearanceChange {
    static GLFWColorScheme previously_reported_appearance = GLFW_COLOR_SCHEME_NO_PREFERENCE;
    GLFWColorScheme new_appearance = glfwGetCurrentSystemColorTheme(true);
    if (new_appearance != previously_reported_appearance) {
        previously_reported_appearance = new_appearance;
        _glfwInputColorScheme(new_appearance, false);
    }
}

- (void)applicationWillTerminate:(NSNotification *)aNotification
{
    (void)aNotification;
    CGDisplayRemoveReconfigurationCallback(display_reconfigured, NULL);
    @try {
        [[NSApplication sharedApplication] removeObserver:self
                                               forKeyPath:@"effectiveAppearance"
                                                  context:AppearanceObservationContext];
    } @catch (NSException * __unused exception) {
        // Ignore exceptions, which can happen if the observer was never added.
    }
}

- (void)applicationDidHide:(NSNotification *)notification
{
    (void)notification;
    int i;

    for (i = 0;  i < _glfw.monitorCount;  i++)
        _glfwRestoreVideoModeNS(_glfw.monitors[i]);
}

@end // GLFWApplicationDelegate
// }}}


@interface GLFWApplication : NSApplication
- (void)tick_callback;
@end

@implementation GLFWApplication
- (void)tick_callback
{
    _glfwDispatchTickCallback();
}

@end


//////////////////////////////////////////////////////////////////////////
//////                       GLFW internal API                      //////
//////////////////////////////////////////////////////////////////////////

void* _glfwLoadLocalVulkanLoaderNS(void)
{
    CFBundleRef bundle = CFBundleGetMainBundle();
    if (!bundle)
        return NULL;

    CFURLRef url =
        CFBundleCopyAuxiliaryExecutableURL(bundle, CFSTR("libvulkan.1.dylib"));
    if (!url)
        return NULL;

    char path[PATH_MAX];
    void* handle = NULL;

    if (CFURLGetFileSystemRepresentation(url, true, (UInt8*) path, sizeof(path) - 1))
        handle = _glfw_dlopen(path);

    CFRelease(url);
    return handle;
}


//////////////////////////////////////////////////////////////////////////
//////                       GLFW platform API                      //////
//////////////////////////////////////////////////////////////////////////

/**
 * Apple Symbolic HotKeys Ids
 * To find this symbolic hot keys indices do:
 * 1. open Terminal
 * 2. restore defaults in System Preferences > Keyboard > Shortcuts
 * 3. defaults read com.apple.symbolichotkeys > current.txt
 * 4. enable/disable given symbolic hot key in System Preferences > Keyboard > Shortcuts
 * 5. defaults read com.apple.symbolichotkeys | diff -C 5 current.txt -
 * 6. restore defaults in System Preferences > Keyboard > Shortcuts
 */
typedef enum AppleShortcutNames {
    // launchpad & dock
    kSHKTurnDockHidingOnOrOff                   = 52,   // Opt, Cmd, D
    kSHKShowLaunchpad                           = 160,  //

    // display
    kSHKDecreaseDisplayBrightness1              = 53,   // F14 (Fn)
    kSHKDecreaseDisplayBrightness2              = 55,   // F14 (Fn, Ctrl)
    kSHKIncreaseDisplayBrightness1              = 54,   // F15 (Fn)
    kSHKIncreaseDisplayBrightness2              = 56,   // F15 (Fn, Ctrl)

    // mission control
    kSHKMissionControl                          = 32,   // Ctrl, Arrow Up
    kSHKShowNotificationCenter                  = 163,  //
    kSHKTurnDoNotDisturbOnOrOff                 = 175,  //
    kSHKApplicationWindows                      = 33,   // Ctrl, Arrow Down
    kSHKShowDesktop                             = 36,   // F11
    kSHKMoveLeftASpace                          = 79,   // Ctrl, Arrow Left
    kSHKMoveRightASpace                         = 81,   // Ctrl, Arrow Right
    kSHKSwitchToDesktop1                        = 118,  // Ctrl, 1
    kSHKSwitchToDesktop2                        = 119,  // Ctrl, 2
    kSHKSwitchToDesktop3                        = 120,  // Ctrl, 3
    kSHKSwitchToDesktop4                        = 121,  // Ctrl, 4
    kSHKQuickNote                               = 190,  // Fn, Q

    // keyboard
    kSHKChangeTheWayTabMovesFocus               = 13,   // Ctrl, F7
    kSHKTurnKeyboardAccessOnOrOff               = 12,   // Ctrl, F1
    kSHKMoveFocusToTheMenuBar                   = 7,    // Ctrl, F2
    kSHKMoveFocusToTheDock                      = 8,    // Ctrl, F3
    kSHKMoveFocusToActiveOrNextWindow           = 9,    // Ctrl, F4
    kSHKMoveFocusToTheWindowToolbar             = 10,   // Ctrl, F5
    kSHKMoveFocusToTheFloatingWindow            = 11,   // Ctrl, F6
    kSHKMoveFocusToNextWindow                   = 27,   // Cmd, `
    kSHKMoveFocusToStatusMenus                  = 57,   // Ctrl, F8

    // input sources
    kSHKSelectThePreviousInputSource            = 60,   // Ctrl, Space bar
    kSHKSelectNextSourceInInputMenu             = 61,   // Ctrl, Opt, Space bar

    // screenshots
    kSHKSavePictureOfScreenAsAFile              = 28,   // Shift, Cmd, 3
    kSHKCopyPictureOfScreenToTheClipboard       = 29,   // Ctrl, Shift, Cmd, 3
    kSHKSavePictureOfSelectedAreaAsAFile        = 30,   // Shift, Cmd, 4
    kSHKCopyPictureOfSelectedAreaToTheClipboard = 31,   // Ctrl, Shift, Cmd, 4
    kSHKScreenshotAndRecordingOptions           = 184,  // Shift, Cmd, 5

    // spotlight
    kSHKShowSpotlightSearch                     = 64,   // Cmd, Space bar
    kSHKShowFinderSearchWindow                  = 65,   // Opt, Cmd, Space bar

    // accessibility
    kSHKTurnZoomOnOrOff                         = 15,   // Opt, Cmd, 8
    kSHKTurnImageSmoothingOnOrOff               = 23,   // Opt, Cmd, Backslash "\"
    kSHKZoomOut                                 = 19,   // Opt, Cmd, -
    kSHKZoomIn                                  = 17,   // Opt, Cmd, =
    kSHKTurnFocusFollowingOnOrOff               = 179,  //

    kSHKIncreaseContrast                        = 25,   // Ctrl, Opt, Cmd, .
    kSHKDecreaseContrast                        = 26,   // Ctrl, Opt, Cmd, ,

    kSHKInvertColors                            = 21,   // Ctrl, Opt, Cmd, 8
    kSHKTurnVoiceOverOnOrOff                    = 59,   // Cmd, F5
    kSHKShowAccessibilityControls               = 162,  // Opt, Cmd, F5

    // app shortcuts
    kSHKShowHelpMenu                            = 98,   // Shift, Cmd, /

    // deprecated (Not shown on macOS Monterey)
    kSHKMoveFocusToTheWindowDrawer              = 51,   // Opt, Cmd, `
    kSHKShowDashboard                           = 62,   // F12
    kSHKLookUpInDictionary                      = 70,   // Shift, Cmd, E
    kSHKHideAndShowFrontRow                     = 73,   // Cmd, Esc
    kSHKActivateSpaces                          = 75,   // F8

    // unknown
    kSHKUnknown                                 = 0,    //
} AppleShortcutNames;

static bool
is_shiftable_shortcut(int scv) {
    return scv == kSHKMoveFocusToActiveOrNextWindow || scv == kSHKMoveFocusToNextWindow;
}

#define USEFUL_MODS(x) (x & (NSEventModifierFlagShift | NSEventModifierFlagOption | NSEventModifierFlagCommand | NSEventModifierFlagControl | NSEventModifierFlagFunction))

static void
build_global_shortcuts_lookup(void) {
    // dump these in a terminal with: defaults read com.apple.symbolichotkeys
    NSMutableDictionary<NSString*, NSNumber*> *temp = [NSMutableDictionary dictionaryWithCapacity:128];  // will be autoreleased
    NSMutableSet<NSNumber*> *temp_configured = [NSMutableSet setWithCapacity:128];  // will be autoreleased
    NSMutableSet<NSNumber*> *temp_missing_value = [NSMutableSet setWithCapacity:128];  // will be autoreleased
    NSDictionary *apple_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.symbolichotkeys"];
    if (apple_settings) {
        NSDictionary<NSString*, id> *symbolic_hotkeys = [apple_settings objectForKey:@"AppleSymbolicHotKeys"];
        if (symbolic_hotkeys) {
            for (NSString *key in symbolic_hotkeys) {
                id obj = symbolic_hotkeys[key];
                if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) continue;
                NSInteger sc = [key integerValue];
                NSDictionary *sc_value = obj;
                id enabled = [sc_value objectForKey:@"enabled"];
                if (!enabled || ![enabled isKindOfClass:[NSNumber class]]) continue;
                [temp_configured addObject:@(sc)];
                if (![enabled boolValue]) continue;
                id v = [sc_value objectForKey:@"value"];
                if (!v || ![v isKindOfClass:[NSDictionary class]]) {
                    if ([enabled boolValue]) [temp_missing_value addObject:@(sc)];
                    continue;
                }
                NSDictionary *value = v;
                id t = [value objectForKey:@"type"];
                if (!t || ![t isKindOfClass:[NSString class]] || ![t isEqualToString:@"standard"]) continue;
                id p = [value objectForKey:@"parameters"];
                if (!p || ![p isKindOfClass:[NSArray class]] || [(NSArray*)p count] < 2) continue;
                NSArray<NSNumber*> *parameters = p;
                NSInteger ch = [parameters[0] isKindOfClass:[NSNumber class]] ? [parameters[0] integerValue] : 0xffff;
                NSInteger vk = [parameters[1] isKindOfClass:[NSNumber class]] ? [parameters[1] integerValue] : 0xffff;
                NSEventModifierFlags mods = ([parameters count] > 2 && [parameters[2] isKindOfClass:[NSNumber class]]) ? [parameters[2] unsignedIntegerValue] : 0;
                mods = USEFUL_MODS(mods);
                static char buf[64];
#define S(x, k) snprintf(buf, sizeof(buf) - 1, #x":%lx:%ld", (unsigned long)mods, (long)k)
                if (ch == 0xffff) { if (vk == 0xffff) continue; S(v, vk); } else S(c, ch);
                temp[@(buf)] = @(sc);
                // the move to next window shortcuts also respond to the same shortcut + shift
                if (is_shiftable_shortcut([key intValue]) && !(mods & NSEventModifierFlagShift)) {
                    mods |= NSEventModifierFlagShift;
                    if (ch == 0xffff) S(v, vk); else S(c, ch);
                    temp[@(buf)] = @(sc);
                }
#undef S
            }
        }
    }

    // Add global shortcut definitions when the default enabled shortcut is not defined,
    // or when the default enabled shortcut is not disabled and is missing a value.
    // Here are the shortcuts that are enabled by default in the standard ANSI (US) layout.
    // macOS provides separate configurations for some languages or keyboards.
    // In general, the rules here will not take effect.
    static char buf[64];
#define S(i, t, m, k) if ([temp_configured member:@(i)] == nil || [temp_missing_value member:@(i)] != nil) { \
        snprintf(buf, sizeof(buf) - 1, #t":%lx:%ld", (unsigned long)m, (long)k); \
        temp[@(buf)] = @(i); \
    }

    // launchpad & dock
    S(kSHKTurnDockHidingOnOrOff, c, (NSEventModifierFlagOption | NSEventModifierFlagCommand), 'd'); // Opt, Cmd, D
    // mission control
    S(kSHKMissionControl, v, NSEventModifierFlagControl, 126); // Ctrl, Arrow Up
    S(kSHKApplicationWindows, v, NSEventModifierFlagControl, 125); // Ctrl, Arrow Down
    // keyboard
    S(kSHKMoveFocusToTheMenuBar, v, NSEventModifierFlagControl, 120); // Ctrl, F2
    S(kSHKMoveFocusToTheDock, v, NSEventModifierFlagControl, 99); // Ctrl, F3
    S(kSHKMoveFocusToActiveOrNextWindow, v, NSEventModifierFlagControl, 118); // Ctrl, F4
    S(kSHKMoveFocusToActiveOrNextWindow, v, (NSEventModifierFlagShift | NSEventModifierFlagControl), 118); // Shift, Ctrl, F4
    S(kSHKMoveFocusToNextWindow, c, NSEventModifierFlagCommand, 96); // Cmd, `
    S(kSHKMoveFocusToNextWindow, c, (NSEventModifierFlagShift | NSEventModifierFlagCommand), 96); // Shift, Cmd, `
    S(kSHKMoveFocusToStatusMenus, v, NSEventModifierFlagControl, 100); // Ctrl, F8
    // input sources
    S(kSHKSelectThePreviousInputSource, c, NSEventModifierFlagControl, 32); // Ctrl, Space bar
    S(kSHKSelectNextSourceInInputMenu, c, (NSEventModifierFlagControl | NSEventModifierFlagOption), 32); // Ctrl, Opt, Space bar
    // spotlight
    S(kSHKShowSpotlightSearch, c, NSEventModifierFlagCommand, 32); // Cmd, Space bar
    S(kSHKShowFinderSearchWindow, c, (NSEventModifierFlagOption | NSEventModifierFlagCommand), 32); // Opt, Cmd, Space bar

#undef S
    global_shortcuts = [[NSDictionary dictionaryWithDictionary:temp] retain];
    /* NSLog(@"global_shortcuts: %@", global_shortcuts); */
}

static int
is_active_apple_global_shortcut(NSEvent *event) {
    if (global_shortcuts == nil) build_global_shortcuts_lookup();
    NSEventModifierFlags modifierFlags = USEFUL_MODS([event modifierFlags]);
    static char lookup_key[64];
#define LOOKUP(t, k) \
    snprintf(lookup_key, sizeof(lookup_key) - 1, #t":%lx:%ld", (unsigned long)modifierFlags, (long)k); \
    NSNumber *sc = global_shortcuts[@(lookup_key)]; \
    if (sc != nil) return [sc intValue]; \

    if ([event.charactersIgnoringModifiers length] == 1) {
        if (modifierFlags & NSEventModifierFlagShift) {
            const uint32_t ch_without_shift = vk_to_unicode_key_with_current_layout([event keyCode]);
            if (ch_without_shift < GLFW_FKEY_FIRST || ch_without_shift > GLFW_FKEY_LAST) {
                LOOKUP(c, ch_without_shift);
            }
        }
        const unichar ch = [event.charactersIgnoringModifiers characterAtIndex:0];
        LOOKUP(c, ch);
    }
    unsigned short vk = [event keyCode];
    if (vk != 0xffff) {
        LOOKUP(v, vk);
    }
#undef LOOKUP
    return kSHKUnknown;
}

static bool
is_useful_apple_global_shortcut(int sc) {
    switch(sc) {
        // launchpad & dock
        case kSHKTurnDockHidingOnOrOff:                   // Opt, Cmd, D
        case kSHKShowLaunchpad:                           //

        // display
        case kSHKDecreaseDisplayBrightness1:              // F14 (Fn)
        case kSHKDecreaseDisplayBrightness2:              // F14 (Fn, Ctrl)
        case kSHKIncreaseDisplayBrightness1:              // F15 (Fn)
        case kSHKIncreaseDisplayBrightness2:              // F14 (Fn, Ctrl)

        // mission control
        case kSHKMissionControl:                          // Ctrl, Arrow Up
        case kSHKShowNotificationCenter:                  //
        case kSHKTurnDoNotDisturbOnOrOff:                 //
        case kSHKApplicationWindows:                      // Ctrl, Arrow Down
        case kSHKShowDesktop:                             // F11
        case kSHKMoveLeftASpace:                          // Ctrl, Arrow Left
        case kSHKMoveRightASpace:                         // Ctrl, Arrow Right
        case kSHKSwitchToDesktop1:                        // Ctrl, 1
        case kSHKSwitchToDesktop2:                        // Ctrl, 2
        case kSHKSwitchToDesktop3:                        // Ctrl, 3
        case kSHKSwitchToDesktop4:                        // Ctrl, 4
        case kSHKQuickNote:                               // Fn, Q

        // keyboard
        /* case kSHKChangeTheWayTabMovesFocus:               // Ctrl, F7 */
        /* case kSHKTurnKeyboardAccessOnOrOff:               // Ctrl, F1 */
        case kSHKMoveFocusToTheMenuBar:                   // Ctrl, F2
        case kSHKMoveFocusToTheDock:                      // Ctrl, F3
        case kSHKMoveFocusToActiveOrNextWindow:           // Ctrl, F4
        /* case kSHKMoveFocusToTheWindowToolbar:             // Ctrl, F5 */
        /* case kSHKMoveFocusToTheFloatingWindow:            // Ctrl, F6 */
        case kSHKMoveFocusToNextWindow:                   // Cmd, `
        case kSHKMoveFocusToStatusMenus:                  // Ctrl, F8

        // input sources
        case kSHKSelectThePreviousInputSource:            // Ctrl, Space bar
        case kSHKSelectNextSourceInInputMenu:             // Ctrl, Opt, Space bar

        // screenshots
        /* case kSHKSavePictureOfScreenAsAFile:              // Shift, Cmd, 3 */
        /* case kSHKCopyPictureOfScreenToTheClipboard:       // Ctrl, Shift, Cmd, 3 */
        /* case kSHKSavePictureOfSelectedAreaAsAFile:        // Shift, Cmd, 4 */
        /* case kSHKCopyPictureOfSelectedAreaToTheClipboard: // Ctrl, Shift, Cmd, 4 */
        /* case kSHKScreenshotAndRecordingOptions:           // Shift, Cmd, 5 */

        // spotlight
        case kSHKShowSpotlightSearch:                     // Cmd, Space bar
        case kSHKShowFinderSearchWindow:                  // Opt, Cmd, Space bar

        // accessibility
        /* case kSHKTurnZoomOnOrOff:                         // Opt, Cmd, 8 */
        /* case kSHKTurnImageSmoothingOnOrOff:               // Opt, Cmd, Backslash "\" */
        /* case kSHKZoomOut:                                 // Opt, Cmd, - */
        /* case kSHKZoomIn:                                  // Opt, Cmd, = */
        /* case kSHKTurnFocusFollowingOnOrOff:               // */
        /* case kSHKIncreaseContrast:                        // Ctrl, Opt, Cmd, . */
        /* case kSHKDecreaseContrast:                        // Ctrl, Opt, Cmd, , */
        /* case kSHKInvertColors:                            // Ctrl, Opt, Cmd, 8 */
        /* case kSHKTurnVoiceOverOnOrOff:                    // Cmd, F5 */
        /* case kSHKShowAccessibilityControls:               // Opt, Cmd, F5 */

        // app shortcuts
        /* case kSHKShowHelpMenu:                            // Shift, Cmd, / */

        // deprecated (Not shown on macOS Monterey)
        /* case kSHKMoveFocusToTheWindowDrawer:              // Opt, Cmd, ` */
        /* case kSHKShowDashboard:                           // F12 */
        /* case kSHKLookUpInDictionary:                      // Shift, Cmd, E */
        /* case kSHKHideAndShowFrontRow:                     // Cmd, Esc */
        /* case kSHKActivateSpaces:                          // F8 */
            return true;
        default:
            return false;
    }
}

static bool
is_apple_jis_layout_function_key(NSEvent *event) {
    return [event keyCode] == 0x66 /* kVK_JIS_Eisu */ || [event keyCode] == 0x68 /* kVK_JIS_Kana */;
}

static bool
has_apple_fn_global_shortcut(void) {
    NSDictionary *hitoolbox_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.HIToolbox"];
    id obj = [hitoolbox_settings objectForKey:@"AppleFnUsageType"];
    if (![obj isKindOfClass:[NSNumber class]]) return false;
    // Non-zero AppleFnUsageType means macOS has reserved Fn/Globe for a
    // system action such as input source switching, emoji picker, or dictation.
    return [obj integerValue] != 0;
}

static bool
is_apple_fn_global_shortcut(NSEvent *event) {
    if ([event keyCode] != 0x3f /* kVK_Function */) return false;
    NSEventModifierFlags mods = USEFUL_MODS([event modifierFlags]);
    if (mods != 0 && mods != NSEventModifierFlagFunction) return false;
    return has_apple_fn_global_shortcut();
}

GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
    GLFWapplicationshouldhandlereopenfun previous = handle_reopen_callback;
    handle_reopen_callback = callback;
    return previous;
}

GLFWAPI GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback) {
    GLFWapplicationwillfinishlaunchingfun previous = finish_launching_callback;
    finish_launching_callback = callback;
    return previous;
}

int _glfwPlatformInit(bool *supports_window_occlusion)
{
    @autoreleasepool {

    *supports_window_occlusion = true;
    _glfw.ns.helper = [[GLFWHelper alloc] init];

    [NSThread detachNewThreadSelector:@selector(doNothing:)
                             toTarget:_glfw.ns.helper
                           withObject:nil];

    if (NSApp)
        _glfw.ns.finishedLaunching = true;

    [GLFWApplication sharedApplication];

    _glfw.ns.delegate = [[GLFWApplicationDelegate alloc] init];
    if (_glfw.ns.delegate == nil)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to create application delegate");
        return false;
    }

    [NSApp setDelegate:_glfw.ns.delegate];
    [[[NSWorkspace sharedWorkspace] notificationCenter]
        addObserver:_glfw.ns.delegate
        selector:@selector(applicationDidActivate:)
        name:NSWorkspaceDidActivateApplicationNotification
        object:nil];
    static struct {
        unsigned short virtual_key_code;
        NSEventModifierFlags input_source_switch_modifiers;
        NSTimeInterval timestamp;
    } last_keydown_shortcut_event;
    last_keydown_shortcut_event.virtual_key_code = 0xffff;
    last_keydown_shortcut_event.input_source_switch_modifiers = 0;

    NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
    {
        debug_input("---------------- key down -------------------\n");
        debug_input("%s\n", [[event description] UTF8String]);
        if (!_glfw.ignoreOSKeyboardProcessing && !_glfw.keyboard_grabbed) {
            // first check if there is a global menu bar shortcut
            if ([[NSApp mainMenu] performKeyEquivalent:event]) {
                debug_input("keyDown triggered global menu bar action ignoring\n");
                last_keydown_shortcut_event.virtual_key_code = [event keyCode];
                last_keydown_shortcut_event.input_source_switch_modifiers = 0;
                last_keydown_shortcut_event.timestamp = [event timestamp];
                return nil;
            }
            // now check if there is a useful apple shortcut
            int global_shortcut = is_active_apple_global_shortcut(event);
            if (is_useful_apple_global_shortcut(global_shortcut)) {
                debug_input("keyDown triggered global macOS shortcut ignoring\n");
                last_keydown_shortcut_event.virtual_key_code = [event keyCode];
                // record the modifier keys if switching to the next input source
                last_keydown_shortcut_event.input_source_switch_modifiers = (global_shortcut == kSHKSelectNextSourceInInputMenu) ? USEFUL_MODS([event modifierFlags]) : 0;
                last_keydown_shortcut_event.timestamp = [event timestamp];
                return event;
            }
            // check for JIS keyboard layout function keys
            if (is_apple_jis_layout_function_key(event)) {
                debug_input("keyDown triggered JIS layout function key ignoring\n");
                last_keydown_shortcut_event.virtual_key_code = [event keyCode];
                last_keydown_shortcut_event.input_source_switch_modifiers = 0;
                last_keydown_shortcut_event.timestamp = [event timestamp];
                return event;
            }
        }
        last_keydown_shortcut_event.virtual_key_code = 0xffff;
        NSWindow *kw = [NSApp keyWindow];
        if (kw && kw.contentView) [kw.contentView keyDown:event];
        else debug_input("keyDown ignored as no keyWindow present\n");
        return nil;
    };

    NSEvent* (^keyup_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
    {
        debug_input("----------------- key up --------------------\n");
        debug_input("%s\n", [[event description] UTF8String]);
        if (last_keydown_shortcut_event.virtual_key_code != 0xffff && last_keydown_shortcut_event.virtual_key_code == [event keyCode]) {
            // ignore as the corresponding key down event triggered a menu bar or macOS shortcut
            last_keydown_shortcut_event.virtual_key_code = 0xffff;
            debug_input("keyUp ignored as corresponds to previous keyDown that triggered a shortcut\n");
            return nil;
        }
        NSWindow *kw = [NSApp keyWindow];
        if (kw && kw.contentView) [kw.contentView keyUp:event];
        else debug_input("keyUp ignored as no keyWindow present\n");
        return nil;
    };

    NSEvent* (^flags_changed_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
    {
        debug_input("-------------- flags changed -----------------\n");
        debug_input("%s\n", [[event description] UTF8String]);
        last_keydown_shortcut_event.virtual_key_code = 0xffff;
        if (!_glfw.ignoreOSKeyboardProcessing && !_glfw.keyboard_grabbed && is_apple_fn_global_shortcut(event)) {
            debug_input("flagsChanged triggered global fn shortcut ignoring\n");
            return event;
        }
        // switching to the next input source is only confirmed when all modifier keys are released
        if (last_keydown_shortcut_event.input_source_switch_modifiers) {
            if (!([event modifierFlags] & last_keydown_shortcut_event.input_source_switch_modifiers))
                last_keydown_shortcut_event.input_source_switch_modifiers = 0;
            return event;
        }
        NSWindow *kw = [NSApp keyWindow];
        if (kw && kw.contentView) [kw.contentView flagsChanged:event];
        else debug_input("flagsChanged ignored as no keyWindow present\n");
        return nil;
    };

    _glfw.ns.keyUpMonitor =
        [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp
                                              handler:keyup_block];
    _glfw.ns.keyDownMonitor =
        [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
                                              handler:keydown_block];
    _glfw.ns.flagsChangedMonitor =
        [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskFlagsChanged
                                              handler:flags_changed_block];

    if (_glfw.hints.init.ns.chdir)
        changeToResourcesDirectory();

    [[NSUserDefaults standardUserDefaults] registerDefaults:@{
        // Press and Hold prevents some keys from emitting repeated characters
        @"ApplePressAndHoldEnabled": @NO,
        // Dont generate openFile events from command line arguments
        @"NSTreatUnknownArgumentsAsOpen": @"NO",
        // This Tahoe nonsense causes slowdowns in some situations, see for example:
        // https://issues.chromium.org/issues/452372350 it doesnt affect
        // autofill via Edit->Autofill
        @"NSAutoFillHeuristicControllerEnabled" : @NO,
    }];

    NSUserDefaults *apple_settings = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.symbolichotkeys"];
    [apple_settings addObserver:_glfw.ns.helper
                     forKeyPath:@"AppleSymbolicHotKeys"
                        options:NSKeyValueObservingOptionNew
                        context:NULL];
    _glfw.ns.appleSettings = apple_settings;

    [[NSNotificationCenter defaultCenter]
        addObserver:_glfw.ns.helper
           selector:@selector(selectedKeyboardInputSourceChanged:)
               name:NSTextInputContextKeyboardSelectionDidChangeNotification
             object:nil];

    _glfw.ns.eventSource = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
    if (!_glfw.ns.eventSource)
        return false;

    CGEventSourceSetLocalEventsSuppressionInterval(_glfw.ns.eventSource, 0.0);

    if (!initializeTIS())
        return false;

    _glfwPollMonitorsNS();
    return true;

    } // autoreleasepool
}
static NSDate*
get_process_start_time(pid_t pid) {
    struct kinfo_proc kp;
    size_t len = sizeof(kp);
    int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid };

    if (sysctl(mib, 4, &kp, &len, NULL, 0) == 0 && len == sizeof(kp)) {
        struct timeval start_tv = kp.kp_proc.p_starttime;
        time_t start_sec = start_tv.tv_sec;
        suseconds_t start_usec = start_tv.tv_usec;
        NSTimeInterval t = (NSTimeInterval)start_sec + ((NSTimeInterval)start_usec / 1000000.0);
        return [NSDate dateWithTimeIntervalSince1970:t];
    }
    return nil;
}

void _glfwPlatformTerminate(void)
{
    @autoreleasepool {

    // Kill the AutoFill helper process that macOS Tahoe starts and fails to
    // shutdown on application exit, see https://github.com/kovidgoyal/kitty/issues/9299
    // Only kill helpers that were launched within a few seconds of this process to
    // avoid killing helpers from other processes. This is obviously not robust
    // but since Apple cant design its way out of a paper bag, it's the best we
    // can do.
    if (application_finished_launching_at != nil) {
        for (NSRunningApplication *app in [[NSWorkspace sharedWorkspace] runningApplications]) {
            if ([app.bundleIdentifier isEqualToString:@"com.apple.SafariPlatformSupport.Helper"] &&
                [[app.localizedName lowercaseString] containsString:@"autofill (kitty)"]) {
                NSDate *st = get_process_start_time(app.processIdentifier);
                if (st != nil) {
                    NSTimeInterval timeDifference = [application_finished_launching_at timeIntervalSinceDate:st];
                    [st release];
                    if (fabs(timeDifference) <= 5) [app forceTerminate];
                }
            }
        }
        [application_finished_launching_at release]; application_finished_launching_at = nil;
    }

    _glfwClearDisplayLinks();

    if (_glfw.ns.inputSource)
    {
        CFRelease(_glfw.ns.inputSource);
        _glfw.ns.inputSource = NULL;
        _glfw.ns.unicodeData = nil;
    }

    if (_glfw.ns.eventSource)
    {
        CFRelease(_glfw.ns.eventSource);
        _glfw.ns.eventSource = NULL;
    }

    if (_glfw.ns.delegate)
    {
        [NSApp setDelegate:nil];
        [_glfw.ns.delegate release];
        _glfw.ns.delegate = nil;
    }

    if (_glfw.ns.helper)
    {
        [[NSNotificationCenter defaultCenter]
            removeObserver:_glfw.ns.helper
                      name:NSTextInputContextKeyboardSelectionDidChangeNotification
                    object:nil];
        [[NSNotificationCenter defaultCenter]
            removeObserver:_glfw.ns.helper];
        if (_glfw.ns.appleSettings)
            [_glfw.ns.appleSettings removeObserver:_glfw.ns.helper forKeyPath:@"AppleSymbolicHotKeys"];
        [_glfw.ns.helper release];
        _glfw.ns.helper = nil;
    }

    if (_glfw.ns.keyUpMonitor)
        [NSEvent removeMonitor:_glfw.ns.keyUpMonitor];
    if (_glfw.ns.keyDownMonitor)
        [NSEvent removeMonitor:_glfw.ns.keyDownMonitor];
    if (_glfw.ns.flagsChangedMonitor)
        [NSEvent removeMonitor:_glfw.ns.flagsChangedMonitor];

    if (_glfw.ns.appleSettings != nil) {
        [_glfw.ns.appleSettings release];
        _glfw.ns.appleSettings = nil;
    }

    _glfwTerminateNSGL();
    if (global_shortcuts != nil) { [global_shortcuts release]; global_shortcuts = nil; }

    } // autoreleasepool
}

const char* _glfwPlatformGetVersionString(void)
{
    return _GLFW_VERSION_NUMBER " Cocoa NSGL EGL OSMesa"
#if defined(_GLFW_BUILD_DLL)
        " dynamic"
#endif
        ;
}

static GLFWtickcallback tick_callback = NULL;
static void* tick_callback_data = NULL;
static bool tick_callback_requested = false;
static pthread_t main_thread;
static NSLock *tick_lock = NULL;


void _glfwDispatchTickCallback(void) {
    if (tick_lock && tick_callback) {
        while(true) {
            bool do_call = false;
            [tick_lock lock];
            if (tick_callback_requested) { do_call = true; tick_callback_requested = false; }
            [tick_lock unlock];
            if (do_call) tick_callback(tick_callback_data);
            else break;
        }
    }
}

static void
request_tick_callback(void) {
    if (!tick_callback_requested) {
        tick_callback_requested = true;
        [NSApp performSelectorOnMainThread:@selector(tick_callback) withObject:nil waitUntilDone:NO];
    }
}

void _glfwPlatformPostEmptyEvent(void)
{
    if (pthread_equal(pthread_self(), main_thread)) {
        request_tick_callback();
    } else if (tick_lock) {
        [tick_lock lock];
        request_tick_callback();
        [tick_lock unlock];
    }
}


void _glfwPlatformStopMainLoop(void) {
    [NSApp stop:nil];
    _glfwCocoaPostEmptyEvent();
}

void _glfwPlatformRunMainLoop(GLFWtickcallback callback, void* data) {
    main_thread = pthread_self();
    tick_callback = callback;
    tick_callback_data = data;
    tick_lock = [NSLock new];
    [NSApp run];
    [tick_lock release];
    tick_lock = NULL;
    tick_callback = NULL;
    tick_callback_data = NULL;
}


typedef struct {
    NSTimer *os_timer;
    unsigned long long id;
    bool repeats;
    monotonic_t interval;
    GLFWuserdatafun callback;
    void *callback_data;
    GLFWuserdatafun free_callback_data;
} Timer;

static Timer timers[128] = {{0}};
static size_t num_timers = 0;

static void
remove_timer_at(size_t idx) {
    if (idx < num_timers) {
        Timer *t = timers + idx;
        if (t->os_timer) { [t->os_timer invalidate]; t->os_timer = NULL; }
        if (t->callback_data && t->free_callback_data) { t->free_callback_data(t->id, t->callback_data); t->callback_data = NULL; }
        remove_i_from_array(timers, idx, num_timers);
    }
}

static void schedule_timer(Timer *t) {
    t->os_timer = [NSTimer scheduledTimerWithTimeInterval:monotonic_t_to_s_double(t->interval) repeats:(t->repeats ? YES: NO) block:^(NSTimer *os_timer) {
        for (size_t i = 0; i < num_timers; i++) {
            if (timers[i].os_timer == os_timer) {
                timers[i].callback(timers[i].id, timers[i].callback_data);
                if (!timers[i].repeats) remove_timer_at(i);
                break;
            }
        }
    }];
}

unsigned long long _glfwPlatformAddTimer(monotonic_t interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback) {
    static unsigned long long timer_counter = 0;
    if (num_timers >= sizeof(timers)/sizeof(timers[0]) - 1) {
        _glfwInputError(GLFW_PLATFORM_ERROR, "Too many timers added");
        return 0;
    }
    Timer *t = timers + num_timers++;
    t->id = ++timer_counter;
    t->repeats = repeats;
    t->interval = interval;
    t->callback = callback;
    t->callback_data = callback_data;
    t->free_callback_data = free_callback;
    schedule_timer(t);
    return timer_counter;
}

void _glfwPlatformRemoveTimer(unsigned long long timer_id) {
    for (size_t i = 0; i < num_timers; i++) {
        if (timers[i].id == timer_id) {
            remove_timer_at(i);
            break;
        }
    }
}

void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled) {
    for (size_t i = 0; i < num_timers; i++) {
        if (timers[i].id == timer_id) {
            Timer *t = timers + i;
            if (t->os_timer) { [t->os_timer invalidate]; t->os_timer = NULL; }
            t->interval = interval;
            if (enabled) schedule_timer(t);
            break;
        }
    }
}

void _glfwPlatformInputColorScheme(GLFWColorScheme appearance UNUSED) { }
bool _glfwPlatformGrabKeyboard(bool grab UNUSED) { return true; /* directly uses _glfw.keyboard_grabbed */ }
