//========================================================================
// 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 <Availability.h>
#import <CoreServices/CoreServices.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <float.h>
#include <string.h>


static const char*
polymorphic_string_as_utf8(id string) {
    if (string == nil) return "(nil)";
    NSString* characters;
    if ([string isKindOfClass:[NSAttributedString class]])
        characters = [string string];
    else
        characters = (NSString*) string;
    return [characters UTF8String];
}

static uint32_t
vk_code_to_functional_key_code(uint8_t key_code) {  // {{{
    switch(key_code) {
        /* start vk to functional (auto generated by gen-key-constants.py do not edit) */
        case 0x35: return GLFW_FKEY_ESCAPE;
        case 0x24: return GLFW_FKEY_ENTER;
        case 0x30: return GLFW_FKEY_TAB;
        case 0x33: return GLFW_FKEY_BACKSPACE;
        case 0x72: return GLFW_FKEY_INSERT;
        case 0x75: return GLFW_FKEY_DELETE;
        case 0x7b: return GLFW_FKEY_LEFT;
        case 0x7c: return GLFW_FKEY_RIGHT;
        case 0x7e: return GLFW_FKEY_UP;
        case 0x7d: return GLFW_FKEY_DOWN;
        case 0x74: return GLFW_FKEY_PAGE_UP;
        case 0x79: return GLFW_FKEY_PAGE_DOWN;
        case 0x73: return GLFW_FKEY_HOME;
        case 0x77: return GLFW_FKEY_END;
        case 0x39: return GLFW_FKEY_CAPS_LOCK;
        case 0x47: return GLFW_FKEY_NUM_LOCK;
        case 0x6e: return GLFW_FKEY_MENU;
        case 0x7a: return GLFW_FKEY_F1;
        case 0x78: return GLFW_FKEY_F2;
        case 0x63: return GLFW_FKEY_F3;
        case 0x76: return GLFW_FKEY_F4;
        case 0x60: return GLFW_FKEY_F5;
        case 0x61: return GLFW_FKEY_F6;
        case 0x62: return GLFW_FKEY_F7;
        case 0x64: return GLFW_FKEY_F8;
        case 0x65: return GLFW_FKEY_F9;
        case 0x6d: return GLFW_FKEY_F10;
        case 0x67: return GLFW_FKEY_F11;
        case 0x6f: return GLFW_FKEY_F12;
        case 0x69: return GLFW_FKEY_F13;
        case 0x6b: return GLFW_FKEY_F14;
        case 0x71: return GLFW_FKEY_F15;
        case 0x6a: return GLFW_FKEY_F16;
        case 0x40: return GLFW_FKEY_F17;
        case 0x4f: return GLFW_FKEY_F18;
        case 0x50: return GLFW_FKEY_F19;
        case 0x5a: return GLFW_FKEY_F20;
        case 0x52: return GLFW_FKEY_KP_0;
        case 0x53: return GLFW_FKEY_KP_1;
        case 0x54: return GLFW_FKEY_KP_2;
        case 0x55: return GLFW_FKEY_KP_3;
        case 0x56: return GLFW_FKEY_KP_4;
        case 0x57: return GLFW_FKEY_KP_5;
        case 0x58: return GLFW_FKEY_KP_6;
        case 0x59: return GLFW_FKEY_KP_7;
        case 0x5b: return GLFW_FKEY_KP_8;
        case 0x5c: return GLFW_FKEY_KP_9;
        case 0x41: return GLFW_FKEY_KP_DECIMAL;
        case 0x4b: return GLFW_FKEY_KP_DIVIDE;
        case 0x43: return GLFW_FKEY_KP_MULTIPLY;
        case 0x4e: return GLFW_FKEY_KP_SUBTRACT;
        case 0x45: return GLFW_FKEY_KP_ADD;
        case 0x4c: return GLFW_FKEY_KP_ENTER;
        case 0x51: return GLFW_FKEY_KP_EQUAL;
        case 0x38: return GLFW_FKEY_LEFT_SHIFT;
        case 0x3b: return GLFW_FKEY_LEFT_CONTROL;
        case 0x3a: return GLFW_FKEY_LEFT_ALT;
        case 0x37: return GLFW_FKEY_LEFT_SUPER;
        case 0x3c: return GLFW_FKEY_RIGHT_SHIFT;
        case 0x3e: return GLFW_FKEY_RIGHT_CONTROL;
        case 0x3d: return GLFW_FKEY_RIGHT_ALT;
        case 0x36: return GLFW_FKEY_RIGHT_SUPER;
/* end vk to functional */
        default:
            return 0;
    }
} // }}}

static uint32_t
vk_code_to_unicode(uint8_t key_code) {  // {{{
    switch(key_code) {
        /* start vk to unicode (auto generated by gen-key-constants.py do not edit) */
        case 0x0: return 0x61;
        case 0x1: return 0x73;
        case 0x2: return 0x64;
        case 0x3: return 0x66;
        case 0x4: return 0x68;
        case 0x5: return 0x67;
        case 0x6: return 0x7a;
        case 0x7: return 0x78;
        case 0x8: return 0x63;
        case 0x9: return 0x76;
        case 0xb: return 0x62;
        case 0xc: return 0x71;
        case 0xd: return 0x77;
        case 0xe: return 0x65;
        case 0xf: return 0x72;
        case 0x10: return 0x79;
        case 0x11: return 0x74;
        case 0x12: return 0x31;
        case 0x13: return 0x32;
        case 0x14: return 0x33;
        case 0x15: return 0x34;
        case 0x16: return 0x36;
        case 0x17: return 0x35;
        case 0x18: return 0x3d;
        case 0x19: return 0x39;
        case 0x1a: return 0x37;
        case 0x1b: return 0x2d;
        case 0x1c: return 0x38;
        case 0x1d: return 0x30;
        case 0x1e: return 0x5d;
        case 0x1f: return 0x6f;
        case 0x20: return 0x75;
        case 0x21: return 0x5b;
        case 0x22: return 0x69;
        case 0x23: return 0x70;
        case 0x25: return 0x6c;
        case 0x26: return 0x6a;
        case 0x27: return 0x27;
        case 0x28: return 0x6b;
        case 0x29: return 0x3b;
        case 0x2a: return 0x5c;
        case 0x2b: return 0x2c;
        case 0x2c: return 0x2f;
        case 0x2d: return 0x6e;
        case 0x2e: return 0x6d;
        case 0x2f: return 0x2e;
        case 0x31: return 0x20;
        case 0x32: return 0x60;
/* end vk to unicode */
        default:
            return 0;
    }
} // }}}

static uint32_t
mac_ucode_to_functional(uint32_t key_code) {  // {{{
    switch(key_code) {
        /* start macu to functional (auto generated by gen-key-constants.py do not edit) */
        case NSCarriageReturnCharacter: return GLFW_FKEY_ENTER;
        case NSTabCharacter: return GLFW_FKEY_TAB;
        case NSBackspaceCharacter: return GLFW_FKEY_BACKSPACE;
        case NSInsertFunctionKey: return GLFW_FKEY_INSERT;
        case NSDeleteFunctionKey: return GLFW_FKEY_DELETE;
        case NSLeftArrowFunctionKey: return GLFW_FKEY_LEFT;
        case NSRightArrowFunctionKey: return GLFW_FKEY_RIGHT;
        case NSUpArrowFunctionKey: return GLFW_FKEY_UP;
        case NSDownArrowFunctionKey: return GLFW_FKEY_DOWN;
        case NSPageUpFunctionKey: return GLFW_FKEY_PAGE_UP;
        case NSPageDownFunctionKey: return GLFW_FKEY_PAGE_DOWN;
        case NSHomeFunctionKey: return GLFW_FKEY_HOME;
        case NSEndFunctionKey: return GLFW_FKEY_END;
        case NSScrollLockFunctionKey: return GLFW_FKEY_SCROLL_LOCK;
        case NSClearLineFunctionKey: return GLFW_FKEY_NUM_LOCK;
        case NSPrintScreenFunctionKey: return GLFW_FKEY_PRINT_SCREEN;
        case NSPauseFunctionKey: return GLFW_FKEY_PAUSE;
        case NSMenuFunctionKey: return GLFW_FKEY_MENU;
        case NSF1FunctionKey: return GLFW_FKEY_F1;
        case NSF2FunctionKey: return GLFW_FKEY_F2;
        case NSF3FunctionKey: return GLFW_FKEY_F3;
        case NSF4FunctionKey: return GLFW_FKEY_F4;
        case NSF5FunctionKey: return GLFW_FKEY_F5;
        case NSF6FunctionKey: return GLFW_FKEY_F6;
        case NSF7FunctionKey: return GLFW_FKEY_F7;
        case NSF8FunctionKey: return GLFW_FKEY_F8;
        case NSF9FunctionKey: return GLFW_FKEY_F9;
        case NSF10FunctionKey: return GLFW_FKEY_F10;
        case NSF11FunctionKey: return GLFW_FKEY_F11;
        case NSF12FunctionKey: return GLFW_FKEY_F12;
        case NSF13FunctionKey: return GLFW_FKEY_F13;
        case NSF14FunctionKey: return GLFW_FKEY_F14;
        case NSF15FunctionKey: return GLFW_FKEY_F15;
        case NSF16FunctionKey: return GLFW_FKEY_F16;
        case NSF17FunctionKey: return GLFW_FKEY_F17;
        case NSF18FunctionKey: return GLFW_FKEY_F18;
        case NSF19FunctionKey: return GLFW_FKEY_F19;
        case NSF20FunctionKey: return GLFW_FKEY_F20;
        case NSF21FunctionKey: return GLFW_FKEY_F21;
        case NSF22FunctionKey: return GLFW_FKEY_F22;
        case NSF23FunctionKey: return GLFW_FKEY_F23;
        case NSF24FunctionKey: return GLFW_FKEY_F24;
        case NSF25FunctionKey: return GLFW_FKEY_F25;
        case NSF26FunctionKey: return GLFW_FKEY_F26;
        case NSF27FunctionKey: return GLFW_FKEY_F27;
        case NSF28FunctionKey: return GLFW_FKEY_F28;
        case NSF29FunctionKey: return GLFW_FKEY_F29;
        case NSF30FunctionKey: return GLFW_FKEY_F30;
        case NSF31FunctionKey: return GLFW_FKEY_F31;
        case NSF32FunctionKey: return GLFW_FKEY_F32;
        case NSF33FunctionKey: return GLFW_FKEY_F33;
        case NSF34FunctionKey: return GLFW_FKEY_F34;
        case NSF35FunctionKey: return GLFW_FKEY_F35;
        case NSEnterCharacter: return GLFW_FKEY_KP_ENTER;
/* end macu to functional */
        default:
            return 0;
    }
} // }}}

static bool
is_surrogate(UniChar uc) { return (uc - 0xd800u) < 2048u; }

static uint32_t
get_first_codepoint(UniChar *utf16, UniCharCount num) {
    if (!num) return 0;
    if (!is_surrogate(*utf16)) return *utf16;
    if (CFStringIsSurrogateHighCharacter(*utf16) && num > 1 && CFStringIsSurrogateLowCharacter(utf16[1])) return CFStringGetLongCharacterForSurrogatePair(utf16[0], utf16[1]);
    return 0;
}

static bool
is_pua_char(uint32_t ch) {
    return (0xE000 <= ch && ch <= 0xF8FF) || (0xF0000 <= ch && ch <= 0xFFFFF) || (0x100000 <= ch && ch <= 0x10FFFF);
}

uint32_t
vk_to_unicode_key_with_current_layout(uint16_t keycode)
{
    UInt32 dead_key_state = 0;
    UniChar characters[256];
    UniCharCount character_count = 0;
    uint32_t ans = vk_code_to_functional_key_code(keycode);
    if (ans) return ans;

    if (UCKeyTranslate([(NSData*) _glfw.ns.unicodeData bytes],
                       keycode,
                       kUCKeyActionDisplay,
                       0,
                       LMGetKbdType(),
                       kUCKeyTranslateNoDeadKeysBit,
                       &dead_key_state,
                       arraysz(characters),
                       &character_count,
                       characters) == noErr) {
        uint32_t cp = get_first_codepoint(characters, character_count);
        if (cp) {
            if (cp < 32 || (0xF700 <= cp && cp <= 0xF8FF)) return mac_ucode_to_functional(cp);
            if (cp >= 32 && !is_pua_char(cp)) return cp;
        }
    }
    return vk_code_to_unicode(keycode);
}


// Returns the style mask corresponding to the window settings
//
static NSUInteger getStyleMask(_GLFWwindow* window)
{
    NSUInteger styleMask = NSWindowStyleMaskMiniaturizable;

    if (window->monitor || !window->decorated)
        styleMask |= NSWindowStyleMaskBorderless;
    else
    {
        styleMask |= NSWindowStyleMaskTitled |
                     NSWindowStyleMaskClosable;

        if (window->resizable)
            styleMask |= NSWindowStyleMaskResizable;
    }

    return styleMask;
}


CGDirectDisplayID displayIDForWindow(_GLFWwindow *w) {
    NSWindow *nw = w->ns.object;
    NSDictionary *dict = [nw.screen deviceDescription];
    NSNumber *displayIDns = dict[@"NSScreenNumber"];
    if (displayIDns) return [displayIDns unsignedIntValue];
    return (CGDirectDisplayID)-1;
}

static unsigned long long display_link_shutdown_timer = 0;
#define DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL s_to_monotonic_t(30ll)

void
_glfwShutdownCVDisplayLink(unsigned long long timer_id UNUSED, void *user_data UNUSED) {
    display_link_shutdown_timer = 0;
    for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
        _GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
        if (dl->displayLink) CVDisplayLinkStop(dl->displayLink);
        dl->lastRenderFrameRequestedAt = 0;
        dl->first_unserviced_render_frame_request_at = 0;
    }
}

static void
requestRenderFrame(_GLFWwindow *w, GLFWcocoarenderframefun callback) {
    if (!callback) {
        w->ns.renderFrameRequested = false;
        w->ns.renderFrameCallback = NULL;
        return;
    }
    w->ns.renderFrameCallback = callback;
    w->ns.renderFrameRequested = true;
    CGDirectDisplayID displayID = displayIDForWindow(w);
    if (display_link_shutdown_timer) {
        _glfwPlatformUpdateTimer(display_link_shutdown_timer, DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL, true);
    } else {
        display_link_shutdown_timer = _glfwPlatformAddTimer(DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL, false, _glfwShutdownCVDisplayLink, NULL, NULL);
    }
    monotonic_t now = glfwGetTime();
    bool found_display_link = false;
    _GLFWDisplayLinkNS *dl = NULL;
    for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
        dl = &_glfw.ns.displayLinks.entries[i];
        if (dl->displayID == displayID) {
            found_display_link = true;
            dl->lastRenderFrameRequestedAt = now;
            if (!dl->first_unserviced_render_frame_request_at) dl->first_unserviced_render_frame_request_at = now;
            if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
            else if (now - dl->first_unserviced_render_frame_request_at > s_to_monotonic_t(1ll)) {
                // display link is stuck need to recreate it because Apple cant even
                // get a simple timer right
                CVDisplayLinkRelease(dl->displayLink); dl->displayLink = nil;
                dl->first_unserviced_render_frame_request_at = now;
                _glfw_create_cv_display_link(dl);
                _glfwInputError(GLFW_PLATFORM_ERROR,
                    "CVDisplayLink stuck possibly because of sleep/screensaver + Apple's incompetence, recreating.");
                if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
            }
        } else if (dl->displayLink && dl->lastRenderFrameRequestedAt && now - dl->lastRenderFrameRequestedAt >= DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL) {
            CVDisplayLinkStop(dl->displayLink);
            dl->lastRenderFrameRequestedAt = 0;
            dl->first_unserviced_render_frame_request_at = 0;
        }
    }
    if (!found_display_link) {
        dl = _glfw_create_display_link(displayID);
        if (dl) {
            dl->lastRenderFrameRequestedAt = now;
            dl->first_unserviced_render_frame_request_at = now;
            if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
        }
    }
}

void
_glfwRestartDisplayLinks(void) {
    _GLFWwindow* window;
    for (window = _glfw.windowListHead;  window;  window = window->next) {
        if (window->ns.renderFrameRequested && window->ns.renderFrameCallback) {
            requestRenderFrame(window, window->ns.renderFrameCallback);
        }
    }
}

// Returns whether the cursor is in the content area of the specified window
//
static bool cursorInContentArea(_GLFWwindow* window)
{
    const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];
    return [window->ns.view mouse:pos inRect:[window->ns.view frame]];
}

// Hides the cursor if not already hidden
//
static void hideCursor(_GLFWwindow* window UNUSED)
{
    if (!_glfw.ns.cursorHidden)
    {
        [NSCursor hide];
        _glfw.ns.cursorHidden = true;
    }
}

// Shows the cursor if not already shown
//
static void showCursor(_GLFWwindow* window UNUSED)
{
    if (_glfw.ns.cursorHidden)
    {
        [NSCursor unhide];
        _glfw.ns.cursorHidden = false;
    }
}

// Updates the cursor image according to its cursor mode
//
static void updateCursorImage(_GLFWwindow* window)
{
    if (window->cursorMode == GLFW_CURSOR_NORMAL)
    {
        showCursor(window);

        if (window->cursor)
            [(NSCursor*) window->cursor->ns.object set];
        else
            [[NSCursor arrowCursor] set];
    }
    else
        hideCursor(window);
}

// Apply chosen cursor mode to a focused window
//
static void updateCursorMode(_GLFWwindow* window)
{
    if (window->cursorMode == GLFW_CURSOR_DISABLED)
    {
        _glfw.ns.disabledCursorWindow = window;
        _glfwPlatformGetCursorPos(window,
                                  &_glfw.ns.restoreCursorPosX,
                                  &_glfw.ns.restoreCursorPosY);
        _glfwCenterCursorInContentArea(window);
        CGAssociateMouseAndMouseCursorPosition(false);
    }
    else if (_glfw.ns.disabledCursorWindow == window)
    {
        _glfw.ns.disabledCursorWindow = NULL;
        CGAssociateMouseAndMouseCursorPosition(true);
        _glfwPlatformSetCursorPos(window,
                                  _glfw.ns.restoreCursorPosX,
                                  _glfw.ns.restoreCursorPosY);
    }

    if (cursorInContentArea(window))
        updateCursorImage(window);
}

// Make the specified window and its video mode active on its monitor
//
static void acquireMonitor(_GLFWwindow* window)
{
    _glfwSetVideoModeNS(window->monitor, &window->videoMode);
    const CGRect bounds = CGDisplayBounds(window->monitor->ns.displayID);
    const NSRect frame = NSMakeRect(bounds.origin.x,
                                    _glfwTransformYNS(bounds.origin.y + bounds.size.height - 1),
                                    bounds.size.width,
                                    bounds.size.height);

    [window->ns.object setFrame:frame display:YES];

    _glfwInputMonitorWindow(window->monitor, window);
}

// Remove the window and restore the original video mode
//
static void releaseMonitor(_GLFWwindow* window)
{
    if (window->monitor->window != window)
        return;

    _glfwInputMonitorWindow(window->monitor, NULL);
    _glfwRestoreVideoModeNS(window->monitor);
}

// Translates macOS key modifiers into GLFW ones
//
static int
translateFlags(NSUInteger flags)
{
    int mods = 0;

    if (flags & NSEventModifierFlagShift)
        mods |= GLFW_MOD_SHIFT;
    if (flags & NSEventModifierFlagControl)
        mods |= GLFW_MOD_CONTROL;
    if (flags & NSEventModifierFlagOption)
        mods |= GLFW_MOD_ALT;
    if (flags & NSEventModifierFlagCommand)
        mods |= GLFW_MOD_SUPER;
    if (flags & NSEventModifierFlagCapsLock)
        mods |= GLFW_MOD_CAPS_LOCK;

    return mods;
}

static const char*
format_mods(int mods) {
    static char buf[128];
    char *p = buf, *s;
#define pr(x) p += snprintf(p, sizeof(buf) - (p - buf) - 1, x)
    pr("mods: ");
    s = p;
    if (mods & GLFW_MOD_CONTROL) pr("ctrl+");
    if (mods & GLFW_MOD_ALT) pr("alt+");
    if (mods & GLFW_MOD_SHIFT) pr("shift+");
    if (mods & GLFW_MOD_SUPER) pr("super+");
    if (mods & GLFW_MOD_CAPS_LOCK) pr("capslock+");
    if (mods & GLFW_MOD_NUM_LOCK) pr("numlock+");
    if (p == s) pr("none");
    else p--;
    pr(" ");
#undef pr
    return buf;
}

static const char*
format_text(const char *src) {
    static char buf[256];
    char *p = buf;
    const char *last_char = buf + sizeof(buf) - 1;
    if (!src[0]) return "<none>";
    while (*src) {
        int num = snprintf(p, sizeof(buf) - (p - buf), "0x%x ", (unsigned char)*(src++));
        if (num < 0) return "<error>";
        if (p + num >= last_char) break;
        p += num;
    }
    if (p != buf) *(--p) = 0;
    return buf;
}

static const char*
safe_name_for_keycode(unsigned int keycode) {
    const char *ans = _glfwPlatformGetNativeKeyName(keycode);
    if (!ans) return "<noname>";
    if ((1 <= ans[0] && ans[0] <= 31) || ans[0] == 127) ans = "<cc>";
    return ans;
}


// Translates a macOS keycode to a GLFW keycode
//
static uint32_t
translateKey(uint16_t vk_key, bool apply_keymap)
{
    if (apply_keymap) return vk_to_unicode_key_with_current_layout(vk_key);
    uint32_t ans = vk_code_to_functional_key_code(vk_key);
    if (!ans) ans = vk_code_to_unicode(vk_key);
    return ans;
}

static NSRect
get_window_size_without_border_in_logical_pixels(_GLFWwindow *window) {
    return [window->ns.object contentRectForFrameRect:[window->ns.object frame]];
}

// Defines a constant for empty ranges in NSTextInputClient
//
static const NSRange kEmptyRange = { NSNotFound, 0 };


// Delegate for window related notifications {{{

@interface GLFWWindowDelegate : NSObject
{
    _GLFWwindow* window;
}

- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
- (void)request_delayed_cursor_update:(id)sender;

@end

@implementation GLFWWindowDelegate

- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow
{
    self = [super init];
    if (self != nil)
        window = initWindow;

    return self;
}

- (BOOL)windowShouldClose:(id)sender
{
    (void)sender;
    _glfwInputWindowCloseRequest(window);
    return NO;
}

- (void)windowDidResize:(NSNotification *)notification
{
    (void)notification;
    if (window->context.client != GLFW_NO_API)
        [window->context.nsgl.object update];

    if (_glfw.ns.disabledCursorWindow == window)
        _glfwCenterCursorInContentArea(window);

    const int maximized = [window->ns.object isZoomed];
    if (window->ns.maximized != maximized)
    {
        window->ns.maximized = maximized;
        _glfwInputWindowMaximize(window, maximized);
    }

    const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
    const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect];

    if (fbRect.size.width != window->ns.fbWidth ||
        fbRect.size.height != window->ns.fbHeight)
    {
        window->ns.fbWidth  = (int)fbRect.size.width;
        window->ns.fbHeight = (int)fbRect.size.height;
        _glfwInputFramebufferSize(window, (int)fbRect.size.width, (int)fbRect.size.height);
    }

    if (contentRect.size.width != window->ns.width ||
        contentRect.size.height != window->ns.height)
    {
        window->ns.width  = (int)contentRect.size.width;
        window->ns.height = (int)contentRect.size.height;
        _glfwInputWindowSize(window, (int)contentRect.size.width, (int)contentRect.size.height);
    }
}

- (void)windowDidMove:(NSNotification *)notification
{
    (void)notification;
    if (window->context.client != GLFW_NO_API)
        [window->context.nsgl.object update];

    if (_glfw.ns.disabledCursorWindow == window)
        _glfwCenterCursorInContentArea(window);

    int x, y;
    _glfwPlatformGetWindowPos(window, &x, &y);
    _glfwInputWindowPos(window, x, y);
}

- (void)windowDidChangeOcclusionState:(NSNotification *)notification
{
    (void)notification;
    _glfwInputWindowOcclusion(window, !([window->ns.object occlusionState] & NSWindowOcclusionStateVisible));
}

- (void)windowDidMiniaturize:(NSNotification *)notification
{
    (void)notification;
    if (window->monitor)
        releaseMonitor(window);

    _glfwInputWindowIconify(window, true);
}

- (void)windowDidDeminiaturize:(NSNotification *)notification
{
    (void)notification;
    if (window->monitor)
        acquireMonitor(window);

    _glfwInputWindowIconify(window, false);
}

- (void)windowDidBecomeKey:(NSNotification *)notification
{
    (void)notification;
    if (_glfw.ns.disabledCursorWindow == window)
        _glfwCenterCursorInContentArea(window);

    _glfwInputWindowFocus(window, true);
    updateCursorMode(window);
    if (window->cursorMode == GLFW_CURSOR_HIDDEN) hideCursor(window);
    if (_glfw.ns.disabledCursorWindow != window && cursorInContentArea(window))
    {
        double x = 0, y = 0;
        _glfwPlatformGetCursorPos(window, &x, &y);
        _glfwInputCursorPos(window, x, y);
    }
    // macOS will send a delayed event to update the cursor to arrow after switching desktops.
    // So we need to delay and update the cursor once after that.
    [self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}

- (void)windowDidResignKey:(NSNotification *)notification
{
    (void)notification;
    if (window->monitor && window->autoIconify)
        _glfwPlatformIconifyWindow(window);
    showCursor(window);

    _glfwInputWindowFocus(window, false);
    // IME is cancelled when losing the focus
    if ([window->ns.view hasMarkedText]) {
        [[window->ns.view inputContext] discardMarkedText];
        [window->ns.view unmarkText];
        GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED};
        _glfwInputKeyboard(window, &dummy);
        _glfw.ns.text[0] = 0;
    }
}

- (void)windowDidChangeScreen:(NSNotification *)notification
{
    (void)notification;
    if (window->ns.renderFrameRequested && window->ns.renderFrameCallback) {
        // Ensure that if the window changed its monitor, CVDisplayLink
        // is running for the new monitor
        requestRenderFrame(window, window->ns.renderFrameCallback);
    }
}

- (void)request_delayed_cursor_update:(id)sender
{
    (void)sender;
    if (window) window->ns.delayed_cursor_update_requested = true;
}

- (void)windowWillEnterFullScreen:(NSNotification *)notification
{
    (void)notification;
    if (window) window->ns.in_fullscreen_transition = true;
}

- (void)windowDidEnterFullScreen:(NSNotification *)notification
{
    (void)notification;
    if (window) window->ns.in_fullscreen_transition = false;
    [self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}

- (void)windowWillExitFullScreen:(NSNotification *)notification
{
    (void)notification;
    if (window) window->ns.in_fullscreen_transition = true;
}

- (void)windowDidExitFullScreen:(NSNotification *)notification
{
    (void)notification;
    if (window) window->ns.in_fullscreen_transition = false;
    [self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}

@end // }}}

// Text input context class for the GLFW content view {{{

@interface GLFWTextInputContext : NSTextInputContext
@end

@implementation GLFWTextInputContext
- (void)doCommandBySelector:(SEL)selector
{
    // interpretKeyEvents: May call insertText: or doCommandBySelector:.
    // With the default macOS keybindings, pressing certain key combinations
    // (e.g. Ctrl+/, Ctrl+Cmd+Down/Left/Right) will produce a beep sound.
    debug_key("\n\tTextInputCtx: doCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
}
@end // }}}

// Content view class for the GLFW window {{{

@interface GLFWContentView : NSView <NSTextInputClient>
{
    _GLFWwindow* window;
    NSTrackingArea* trackingArea;
    GLFWTextInputContext* input_context;
    NSMutableAttributedString* markedText;
    NSRect markedRect;
    bool marked_text_cleared_by_insert;
    int in_key_handler;
    NSString *input_source_at_last_key_event;
}

- (void) removeGLFWWindow;
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;

@end

@implementation GLFWContentView

- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow
{
    self = [super init];
    if (self != nil)
    {
        window = initWindow;
        trackingArea = nil;
        input_context = [[GLFWTextInputContext alloc] initWithClient:self];
        markedText = [[NSMutableAttributedString alloc] init];
        markedRect = NSMakeRect(0.0, 0.0, 0.0, 0.0);
        input_source_at_last_key_event = nil;
        in_key_handler = 0;

        [self updateTrackingAreas];
        [self registerForDraggedTypes:@[NSPasteboardTypeFileURL, NSPasteboardTypeString]];
    }

    return self;
}

- (void)dealloc
{
    [trackingArea release];
    [markedText release];
    if (input_source_at_last_key_event) [input_source_at_last_key_event release];
    [input_context release];
    [super dealloc];
}

- (void) removeGLFWWindow
{
    window = NULL;
}

- (_GLFWwindow*)glfwWindow {
    return window;
}

- (BOOL)isOpaque
{
    return window && [window->ns.object isOpaque];
}

- (BOOL)canBecomeKeyView
{
    return YES;
}

- (BOOL)acceptsFirstResponder
{
    return YES;
}

- (void) viewWillStartLiveResize
{
    if (!window) return;
    _glfwInputLiveResize(window, true);
}

- (void)viewDidEndLiveResize
{
    if (!window) return;
    _glfwInputLiveResize(window, false);
}

- (BOOL)wantsUpdateLayer
{
    return YES;
}

- (void)updateLayer
{
    if (!window) return;
    if (window->context.client != GLFW_NO_API) {
        @try {
            [window->context.nsgl.object update];
        } @catch (NSException *e) {
            _glfwInputError(GLFW_PLATFORM_ERROR,
                            "Failed to update NSGL Context object with error: %s (%s)",
                            [[e name] UTF8String], [[e reason] UTF8String]);
        }
    }

    _glfwInputWindowDamage(window);
}

- (void)cursorUpdate:(NSEvent *)event
{
    (void)event;
    if (window) updateCursorImage(window);
}

- (BOOL)acceptsFirstMouse:(NSEvent *)event
{
    (void)event;
    return NO;  // changed by Kovid, to follow cocoa platform conventions
}

- (void)mouseDown:(NSEvent *)event
{
    if (!window) return;
    _glfwInputMouseClick(window,
                         GLFW_MOUSE_BUTTON_LEFT,
                         GLFW_PRESS,
                         translateFlags([event modifierFlags]));
}

- (void)mouseDragged:(NSEvent *)event
{
    [self mouseMoved:event];
}

- (void)mouseUp:(NSEvent *)event
{
    if (!window) return;
    _glfwInputMouseClick(window,
                         GLFW_MOUSE_BUTTON_LEFT,
                         GLFW_RELEASE,
                         translateFlags([event modifierFlags]));
}

- (void)mouseMoved:(NSEvent *)event
{
    if (!window) return;
    if (window->cursorMode == GLFW_CURSOR_DISABLED)
    {
        const double dx = [event deltaX] - window->ns.cursorWarpDeltaX;
        const double dy = [event deltaY] - window->ns.cursorWarpDeltaY;

        _glfwInputCursorPos(window,
                            window->virtualCursorPosX + dx,
                            window->virtualCursorPosY + dy);
    }
    else
    {
        const NSRect contentRect = [window->ns.view frame];
        // NOTE: The returned location uses base 0,1 not 0,0
        const NSPoint pos = [event locationInWindow];

        _glfwInputCursorPos(window, pos.x, contentRect.size.height - pos.y);
    }

    window->ns.cursorWarpDeltaX = 0;
    window->ns.cursorWarpDeltaY = 0;

    if (window->ns.delayed_cursor_update_requested) {
        window->ns.delayed_cursor_update_requested = false;
        if (cursorInContentArea(window)) updateCursorImage(window);
    }
}

- (void)rightMouseDown:(NSEvent *)event
{
    if (!window) return;
    _glfwInputMouseClick(window,
                         GLFW_MOUSE_BUTTON_RIGHT,
                         GLFW_PRESS,
                         translateFlags([event modifierFlags]));
}

- (void)rightMouseDragged:(NSEvent *)event
{
    [self mouseMoved:event];
}

- (void)rightMouseUp:(NSEvent *)event
{
    if (!window) return;
    _glfwInputMouseClick(window,
                         GLFW_MOUSE_BUTTON_RIGHT,
                         GLFW_RELEASE,
                         translateFlags([event modifierFlags]));
}

- (void)otherMouseDown:(NSEvent *)event
{
    if (!window) return;
    _glfwInputMouseClick(window,
                         (int) [event buttonNumber],
                         GLFW_PRESS,
                         translateFlags([event modifierFlags]));
}

- (void)otherMouseDragged:(NSEvent *)event
{
    [self mouseMoved:event];
}

- (void)otherMouseUp:(NSEvent *)event
{
    if (!window) return;
    _glfwInputMouseClick(window,
                         (int) [event buttonNumber],
                         GLFW_RELEASE,
                         translateFlags([event modifierFlags]));
}

- (void)mouseExited:(NSEvent *)event
{
    (void)event;
    if (!window) return;
    _glfwInputCursorEnter(window, false);
}

- (void)mouseEntered:(NSEvent *)event
{
    (void)event;
    if (!window) return;
    _glfwInputCursorEnter(window, true);
}

- (void)viewDidChangeBackingProperties
{
    if (!window) return;
    const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
    const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect];

    if (fbRect.size.width != window->ns.fbWidth ||
        fbRect.size.height != window->ns.fbHeight)
    {
        window->ns.fbWidth  = (int)fbRect.size.width;
        window->ns.fbHeight = (int)fbRect.size.height;
        _glfwInputFramebufferSize(window, (int)fbRect.size.width, (int)fbRect.size.height);
    }

    const float xscale = fbRect.size.width / contentRect.size.width;
    const float yscale = fbRect.size.height / contentRect.size.height;

    if (xscale != window->ns.xscale || yscale != window->ns.yscale)
    {
        window->ns.xscale = xscale;
        window->ns.yscale = yscale;
        _glfwInputWindowContentScale(window, xscale, yscale);

        if (window->ns.retina && window->ns.layer)
            [window->ns.layer setContentsScale:[window->ns.object backingScaleFactor]];
    }
}

- (void)drawRect:(NSRect)rect
{
    (void)rect;
    if (!window) return;
    _glfwInputWindowDamage(window);
}

- (void)updateTrackingAreas
{
    if (window && [window->ns.object areCursorRectsEnabled])
        [window->ns.object disableCursorRects];
    if (trackingArea != nil)
    {
        [self removeTrackingArea:trackingArea];
        [trackingArea release];
    }

    const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
                                          NSTrackingActiveAlways |
                                          NSTrackingEnabledDuringMouseDrag |
                                          NSTrackingCursorUpdate |
                                          NSTrackingInVisibleRect |
                                          NSTrackingAssumeInside;

    trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
                                                options:options
                                                  owner:self
                                               userInfo:nil];

    [self addTrackingArea:trackingArea];
    [super updateTrackingAreas];
}

- (NSTextInputContext *)inputContext
{
    return input_context;
}

static UInt32
convert_cocoa_to_carbon_modifiers(NSUInteger flags) {
    UInt32 mods = 0;
    if (flags & NSEventModifierFlagShift)
        mods |= shiftKey;
    if (flags & NSEventModifierFlagControl)
        mods |= controlKey;
    if (flags & NSEventModifierFlagOption)
        mods |= optionKey;
    if (flags & NSEventModifierFlagCommand)
        mods |= cmdKey;
    if (flags & NSEventModifierFlagCapsLock)
        mods |= alphaLock;

    return (mods >> 8) & 0xFF;
}

static void
convert_utf16_to_utf8(UniChar *src, UniCharCount src_length, char *dest, size_t dest_sz) {
    CFStringRef string = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
                                                            src,
                                                            src_length,
                                                            kCFAllocatorNull);
    CFStringGetCString(string,
                       dest,
                       dest_sz,
                       kCFStringEncodingUTF8);
    CFRelease(string);
}

static bool
alternate_key_is_ok(uint32_t key, uint32_t akey) {
    return akey > 31 && akey != key && !is_pua_char(akey);
}

static void
add_alternate_keys(GLFWkeyevent *ev, NSEvent *event) {
    ev->alternate_key = translateKey(ev->native_key, false);
    if (!alternate_key_is_ok(ev->key, ev->alternate_key)) ev->alternate_key = 0;
    if (ev->mods & GLFW_MOD_SHIFT) {
        NSString *ci = [event charactersIgnoringModifiers];
        if (ci) {
            unsigned sz = [ci length];
            if (sz > 0) {
                UniChar buf[2] = {0};
                buf[0] = [ci characterAtIndex:0];
                if (sz > 1) buf[1] = [ci characterAtIndex:1];
                ev->shifted_key = get_first_codepoint(buf, sz);
            }
        }
        if (!alternate_key_is_ok(ev->key, ev->shifted_key)) ev->shifted_key = 0;
    }
}

static bool
is_ascii_control_char(char x) {
    return x == 0 || (1 <= x && x <= 31) || x == 127;
}

- (void)keyDown:(NSEvent *)event
{
#define CLEAR_PRE_EDIT_TEXT glfw_keyevent.text = NULL; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent);
#define UPDATE_PRE_EDIT_TEXT glfw_keyevent.text = [[markedText string] UTF8String]; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent);

    const bool previous_has_marked_text = [self hasMarkedText];
    if (input_context && (!input_source_at_last_key_event || ![input_source_at_last_key_event isEqualToString:input_context.selectedKeyboardInputSource])) {
        if (input_source_at_last_key_event) {
            debug_key("Input source changed, clearing pre-edit text and resetting deadkey state\n");
            GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED};
            window->ns.deadKeyState = 0;
            _glfwInputKeyboard(window, &dummy); // clear pre-edit text
            [input_source_at_last_key_event release];
            input_source_at_last_key_event = nil;
        }
        input_source_at_last_key_event = [input_context.selectedKeyboardInputSource retain];
        [self unmarkText];
    }

    const unsigned int keycode = [event keyCode];
    const NSUInteger flags = [event modifierFlags];
    const int mods = translateFlags(flags);
    const uint32_t key = translateKey(keycode, true);
    const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, flags) != 1);
    _glfw.ns.text[0] = 0;
    if (keycode == 0x33 /* backspace */ || keycode == 0x35 /* escape */) [self unmarkText];
    GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_PRESS, .mods = mods};
    if (!_glfw.ns.unicodeData) {
        // Using the cocoa API for key handling is disabled, as there is no
        // reliable way to handle dead keys using it. Only use it if the
        // keyboard unicode data is not available.
        if (process_text) {
            // this will call insertText with the text for this event, if any
            [self interpretKeyEvents:@[event]];
        }
    } else {
        static UniChar text[256];
        UniCharCount char_count = 0;
        const bool in_compose_sequence = window->ns.deadKeyState != 0;
        if (UCKeyTranslate(
                    [(NSData*) _glfw.ns.unicodeData bytes],
                    keycode,
                    kUCKeyActionDown,
                    convert_cocoa_to_carbon_modifiers(flags),
                    LMGetKbdType(),
                    (process_text ? 0 : kUCKeyTranslateNoDeadKeysMask),
                    &(window->ns.deadKeyState),
                    sizeof(text)/sizeof(text[0]),
                    &char_count,
                    text
                    ) != noErr) {
            debug_key("UCKeyTranslate failed for keycode: 0x%x (%s) %s\n",
                    keycode, safe_name_for_keycode(keycode), format_mods(mods));
            window->ns.deadKeyState = 0;
            return;
        }
        debug_key("\x1b[31mPress:\x1b[m native_key: 0x%x (%s) glfw_key: 0x%x %schar_count: %lu deadKeyState: %u repeat: %d ",
                keycode, safe_name_for_keycode(keycode), key, format_mods(mods), char_count, window->ns.deadKeyState, event.ARepeat);
        marked_text_cleared_by_insert = false;
        if (process_text) {
            in_key_handler = 1;
            // this will call insertText which will fill up _glfw.ns.text
            [self interpretKeyEvents:@[event]];
            in_key_handler = 0;
        } else {
            window->ns.deadKeyState = 0;
        }
        if (window->ns.deadKeyState && (char_count == 0 || keycode == 0x75)) {
            // 0x75 is the delete key which needs to be ignored during a compose sequence
            debug_key("Sending pre-edit text for dead key (text: %s markedText: %s).\n", format_text(_glfw.ns.text), glfw_keyevent.text);
            UPDATE_PRE_EDIT_TEXT;
            return;
        }
        if (in_compose_sequence) {
            debug_key("Clearing pre-edit text at end of compose sequence\n");
            CLEAR_PRE_EDIT_TEXT;
        }
    }
    if (is_ascii_control_char(_glfw.ns.text[0])) _glfw.ns.text[0] = 0;  // don't send text for ascii control codes
    debug_key("text: %s glfw_key: %s marked_text: (%s)\n",
            format_text(_glfw.ns.text), _glfwGetKeyName(key), [[markedText string] UTF8String]);
    bool bracketed_ime = false;
    if (!window->ns.deadKeyState) {
        if ([self hasMarkedText]) {
            if (!marked_text_cleared_by_insert) {
                UPDATE_PRE_EDIT_TEXT;
            } else bracketed_ime = true;
        } else if (previous_has_marked_text) {
            CLEAR_PRE_EDIT_TEXT;
        }
        if (([self hasMarkedText] || previous_has_marked_text) && !_glfw.ns.text[0]) {
            // do not pass keys like BACKSPACE while there's pre-edit text, let IME handle it
            debug_key("Ignoring key press as IME is active and it generated no text\n");
            return;
        }
    }
    if (bracketed_ime) {
        // insertText followed by setMarkedText
        CLEAR_PRE_EDIT_TEXT;
    }
    glfw_keyevent.text = _glfw.ns.text;
    glfw_keyevent.ime_state = GLFW_IME_NONE;
    add_alternate_keys(&glfw_keyevent, event);
    _glfwInputKeyboard(window, &glfw_keyevent);
    if (bracketed_ime) {
        // insertText followed by setMarkedText
        UPDATE_PRE_EDIT_TEXT;
    }
}

- (void)flagsChanged:(NSEvent *)event
{
    int action = GLFW_RELEASE;
    const char old_first_char = _glfw.ns.text[0];
    _glfw.ns.text[0] = 0;
    const NSUInteger modifierFlags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
    const uint32_t key = vk_code_to_functional_key_code([event keyCode]);
    const unsigned int keycode = [event keyCode];
    const int mods = translateFlags(modifierFlags);
    const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, modifierFlags) != 1);
    const char *mod_name = "unknown";

    switch(key) {
        case GLFW_FKEY_CAPS_LOCK:
            mod_name = "capslock";
            action = modifierFlags & NSEventModifierFlagCapsLock ? GLFW_PRESS : GLFW_RELEASE; break;
        case GLFW_FKEY_LEFT_SUPER:
        case GLFW_FKEY_RIGHT_SUPER:
            mod_name = "super";
            action = modifierFlags & NSEventModifierFlagCommand ? GLFW_PRESS : GLFW_RELEASE; break;
        case GLFW_FKEY_LEFT_CONTROL:
        case GLFW_FKEY_RIGHT_CONTROL:
            mod_name = "ctrl";
            action = modifierFlags & NSEventModifierFlagControl ? GLFW_PRESS : GLFW_RELEASE; break;
        case GLFW_FKEY_LEFT_ALT:
        case GLFW_FKEY_RIGHT_ALT:
            mod_name = "alt";
            action = modifierFlags & NSEventModifierFlagOption ? GLFW_PRESS : GLFW_RELEASE; break;
        case GLFW_FKEY_LEFT_SHIFT:
        case GLFW_FKEY_RIGHT_SHIFT:
            mod_name = "shift";
            action = modifierFlags & NSEventModifierFlagShift ? GLFW_PRESS : GLFW_RELEASE; break;
        default:
            return;
    }
    GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = action, .mods = mods};
    debug_key("\x1b[33mflagsChanged:\x1b[m modifier: %s native_key: 0x%x (%s) glfw_key: 0x%x %s\n",
            mod_name, keycode, safe_name_for_keycode(keycode), key, format_mods(mods));
    marked_text_cleared_by_insert = false;
    if (process_text && input_context) {
        // this will call insertText which will fill up _glfw.ns.text
        in_key_handler = 2;
        [input_context handleEvent:event];
        in_key_handler = 0;
        if (marked_text_cleared_by_insert) {
            debug_key("Clearing pre-edit text because insertText called from flagsChanged\n");
            CLEAR_PRE_EDIT_TEXT;
            if (_glfw.ns.text[0]) glfw_keyevent.text = _glfw.ns.text;
            else _glfw.ns.text[0] = old_first_char;
        }
    }
    glfw_keyevent.ime_state = GLFW_IME_NONE;
    _glfwInputKeyboard(window, &glfw_keyevent);
}

- (void)keyUp:(NSEvent *)event
{
    const uint32_t keycode = [event keyCode];
    const uint32_t key = translateKey(keycode, true);
    const int mods = translateFlags([event modifierFlags]);

    GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_RELEASE, .mods = mods};
    add_alternate_keys(&glfw_keyevent, event);
    debug_key("\x1b[32mRelease:\x1b[m native_key: 0x%x (%s) glfw_key: 0x%x %s\n",
            keycode, safe_name_for_keycode(keycode), key, format_mods(mods));
    _glfwInputKeyboard(window, &glfw_keyevent);
}

#undef CLEAR_PRE_EDIT_TEXT
#undef UPDATE_PRE_EDIT_TEXT

- (void)scrollWheel:(NSEvent *)event
{
    double deltaX = [event scrollingDeltaX];
    double deltaY = [event scrollingDeltaY];

    int flags = [event hasPreciseScrollingDeltas] ? 1 : 0;
    if (flags) {
        float xscale = 1, yscale = 1;
        _glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
        if (xscale > 0) deltaX *= xscale;
        if (yscale > 0) deltaY *= yscale;
    }

    switch([event momentumPhase]) {
        case NSEventPhaseBegan:
            flags |= (1 << 1); break;
        case NSEventPhaseStationary:
            flags |= (2 << 1); break;
        case NSEventPhaseChanged:
            flags |= (3 << 1); break;
        case NSEventPhaseEnded:
            flags |= (4 << 1); break;
        case NSEventPhaseCancelled:
            flags |= (5 << 1); break;
        case NSEventPhaseMayBegin:
            flags |= (6 << 1); break;
        case NSEventPhaseNone:
        default:
            break;
    }

    _glfwInputScroll(window, deltaX, deltaY, flags, translateFlags([event modifierFlags]));
}

- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
    (void)sender;
    // HACK: We don't know what to say here because we don't know what the
    //       application wants to do with the paths
    return NSDragOperationGeneric;
}

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
    const NSRect contentRect = [window->ns.view frame];
    // NOTE: The returned location uses base 0,1 not 0,0
    const NSPoint pos = [sender draggingLocation];
    _glfwInputCursorPos(window, pos.x, contentRect.size.height - pos.y);

    NSPasteboard* pasteboard = [sender draggingPasteboard];
    NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
    NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]]
                                              options:options];
    if (!objs) return NO;
    const NSUInteger count = [objs count];
    NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096];  // auto-released
    if (count)
    {
        for (NSUInteger i = 0;  i < count;  i++)
        {
            id obj = objs[i];
            if ([obj isKindOfClass:[NSURL class]]) {
                NSURL *url = (NSURL*)obj;
                if ([uri_list length] > 0) [uri_list appendString:@("\n")];
                if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString];
                else [uri_list appendString:url.absoluteString];
            } else if ([obj isKindOfClass:[NSString class]]) {
                const char *text = [obj UTF8String];
                _glfwInputDrop(window, "text/plain;charset=utf-8", text, strlen(text));
            } else {
                _glfwInputError(GLFW_PLATFORM_ERROR,
                                "Cocoa: Object is neither a URL nor a string");
            }
        }
    }
    if ([uri_list length] > 0) _glfwInputDrop(window, "text/uri-list", uri_list.UTF8String, strlen(uri_list.UTF8String));

    return YES;
}

- (BOOL)hasMarkedText
{
    return [markedText length] > 0;
}

- (NSRange)markedRange
{
    if ([markedText length] > 0)
        return NSMakeRange(0, [markedText length] - 1);
    else
        return kEmptyRange;
}

- (NSRange)selectedRange
{
    return kEmptyRange;
}

- (void)setMarkedText:(id)string
        selectedRange:(NSRange)selectedRange
     replacementRange:(NSRange)replacementRange
{
    const char *s = polymorphic_string_as_utf8(string);
    debug_key("\n\tsetMarkedText: %s selectedRange: (%lu, %lu) replacementRange: (%lu, %lu)\n", s, selectedRange.location, selectedRange.length, replacementRange.location, replacementRange.length);
    if (string == nil || !s[0]) {
        bool had_marked_text = [self hasMarkedText];
        [self unmarkText];
        if (had_marked_text && (!in_key_handler || in_key_handler == 2)) {
            debug_key("Clearing pre-edit because setMarkedText called from %s\n", in_key_handler ? "flagsChanged" : "event loop");
            GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
            _glfwInputKeyboard(window, &glfw_keyevent);
            _glfw.ns.text[0] = 0;
        }
        return;
    }
    if ([string isKindOfClass:[NSAttributedString class]]) {
        if (((NSMutableAttributedString*)string).length == 0) { [self unmarkText]; return; }
        [markedText release];
        markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
    } else {
        if (((NSString*)string).length == 0) { [self unmarkText]; return; }
        [markedText release];
        markedText = [[NSMutableAttributedString alloc] initWithString:string];
    }
    if (!in_key_handler || in_key_handler == 2) {
        debug_key("Updating IME text in kitty from setMarkedText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text);
        GLFWkeyevent glfw_keyevent = {.text=[[markedText string] UTF8String], .ime_state = GLFW_IME_PREEDIT_CHANGED};
        _glfwInputKeyboard(window, &glfw_keyevent);
        _glfw.ns.text[0] = 0;
    }
}

- (void)unmarkText
{
    [[markedText mutableString] setString:@""];
}

void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
    [w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused left:(CGFloat)ev->cursor.left top:(CGFloat)ev->cursor.top cellWidth:(CGFloat)ev->cursor.width cellHeight:(CGFloat)ev->cursor.height];
}

- (void)updateIMEStateFor:(GLFWIMEUpdateType)which
                  focused:(bool)focused
                     left:(CGFloat)left
                      top:(CGFloat)top
                cellWidth:(CGFloat)cellWidth
               cellHeight:(CGFloat)cellHeight
{
    if (which == GLFW_IME_UPDATE_FOCUS && !focused && [self hasMarkedText] && window) {
        [input_context discardMarkedText];
        [self unmarkText];
        GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
        _glfwInputKeyboard(window, &glfw_keyevent);
        _glfw.ns.text[0] = 0;
    }
    if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return;
    left /= window->ns.xscale;
    top /= window->ns.yscale;
    cellWidth /= window->ns.xscale;
    cellHeight /= window->ns.yscale;
    debug_key("updateIMEPosition: left=%f, top=%f, width=%f, height=%f\n", left, top, cellWidth, cellHeight);
    const NSRect frame = [window->ns.view frame];
    const NSRect rectInView = NSMakeRect(left,
                                         frame.size.height - top - cellHeight,
                                         cellWidth, cellHeight);
    markedRect = [window->ns.object convertRectToScreen: rectInView];
    if (_glfwPlatformWindowFocused(window)) [[window->ns.view inputContext] invalidateCharacterCoordinates];
}

- (NSArray*)validAttributesForMarkedText
{
    return [NSArray array];
}

- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
                                               actualRange:(NSRangePointer)actualRange
{
    (void)range; (void)actualRange;
    return nil;
}

- (NSUInteger)characterIndexForPoint:(NSPoint)point
{
    (void)point;
    return 0;
}

- (NSRect)firstRectForCharacterRange:(NSRange)range
                         actualRange:(NSRangePointer)actualRange
{
    (void)range; (void)actualRange;
    return markedRect;
}

- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
{
    const char *utf8 = polymorphic_string_as_utf8(string);
    debug_key("\n\tinsertText: %s replacementRange: (%lu, %lu)\n", utf8, replacementRange.location, replacementRange.length);
    if ([self hasMarkedText] && !is_ascii_control_char(utf8[0])) {
        [self unmarkText];
        marked_text_cleared_by_insert = true;
        if (!in_key_handler) {
            debug_key("Clearing pre-edit because insertText called from event loop\n");
            GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
            _glfwInputKeyboard(window, &glfw_keyevent);
            _glfw.ns.text[0] = 0;
        }
    }
    // insertText can be called multiple times for a single key event
    char *s = _glfw.ns.text + strnlen(_glfw.ns.text, sizeof(_glfw.ns.text));
    snprintf(s, sizeof(_glfw.ns.text) - (s - _glfw.ns.text), "%s", utf8);
    _glfw.ns.text[sizeof(_glfw.ns.text) - 1] = 0;
    if ((!in_key_handler || in_key_handler == 2) && _glfw.ns.text[0]) {
        if (!is_ascii_control_char(_glfw.ns.text[0])) {
            debug_key("Sending text to kitty from insertText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text);
            GLFWkeyevent glfw_keyevent = {.text=_glfw.ns.text, .ime_state=GLFW_IME_COMMIT_TEXT};
            _glfwInputKeyboard(window, &glfw_keyevent);
        }
        _glfw.ns.text[0] = 0;
    }
}

- (void)doCommandBySelector:(SEL)selector
{
    debug_key("\n\tdoCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
}

- (BOOL)isAccessibilityElement
{
    return YES;
}

- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
{
    if (selector == @selector(accessibilityRole) || selector == @selector(accessibilitySelectedText)) return YES;
    return NO;
}

#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400)

- (NSAccessibilityRole)accessibilityRole
{
    return NSAccessibilityTextAreaRole;
}

#endif

- (NSString *)accessibilitySelectedText
{
    NSString *text = nil;
    if (_glfw.callbacks.get_current_selection) {
        char *s = _glfw.callbacks.get_current_selection();
        if (s) {
            text = [NSString stringWithUTF8String:s];
            free(s);
        }
    }
    return text;
}

@end
// }}}

// GLFW window class {{{

@interface GLFWWindow : NSWindow {
    _GLFWwindow* glfw_window;
}

- (instancetype)initWithGlfwWindow:(NSRect)contentRect
                         styleMask:(NSWindowStyleMask)style
                           backing:(NSBackingStoreType)backingStoreType
                        initWindow:(_GLFWwindow *)initWindow;

- (void) removeGLFWWindow;
@end

@implementation GLFWWindow

- (instancetype)initWithGlfwWindow:(NSRect)contentRect
                         styleMask:(NSWindowStyleMask)style
                           backing:(NSBackingStoreType)backingStoreType
                        initWindow:(_GLFWwindow *)initWindow
{
    self = [super initWithContentRect:contentRect styleMask:style backing:backingStoreType defer:NO];
    if (self != nil) {
        glfw_window = initWindow;
        self.tabbingMode = NSWindowTabbingModeDisallowed;
    }
    return self;
}

- (void) removeGLFWWindow
{
    glfw_window = NULL;
}

- (BOOL)validateMenuItem:(NSMenuItem *)item {
    if (item.action == @selector(performMiniaturize:)) return YES;
    return [super validateMenuItem:item];
}

- (void)performMiniaturize:(id)sender
{
    if (glfw_window && (!glfw_window->decorated || glfw_window->ns.titlebar_hidden)) [self miniaturize:self];
    else [super performMiniaturize:sender];
}

- (BOOL)canBecomeKeyWindow
{
    // Required for NSWindowStyleMaskBorderless windows
    return YES;
}

- (BOOL)canBecomeMainWindow
{
    return YES;
}

- (void)toggleFullScreen:(nullable id)sender
{
    if (glfw_window) {
        if (glfw_window->ns.in_fullscreen_transition) return;
        if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return;
        glfw_window->ns.in_fullscreen_transition = true;
    }
    // When resizeIncrements is set, Cocoa cannot restore the original window size after returning from fullscreen.
    const NSSize original = [self resizeIncrements];
    [self setResizeIncrements:NSMakeSize(1.0, 1.0)];
    [super toggleFullScreen:sender];
    [self setResizeIncrements:original];
    // When the window decoration is hidden, toggling fullscreen causes the style mask to be changed,
    // and causes the first responder to be cleared.
    if (glfw_window && !glfw_window->decorated && glfw_window->ns.view) [self makeFirstResponder:glfw_window->ns.view];
}

@end
// }}}


// Create the Cocoa window
//
static bool createNativeWindow(_GLFWwindow* window,
                                   const _GLFWwndconfig* wndconfig,
                                   const _GLFWfbconfig* fbconfig)
{
    window->ns.delegate = [[GLFWWindowDelegate alloc] initWithGlfwWindow:window];
    if (window->ns.delegate == nil)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to create window delegate");
        return false;
    }

    NSRect contentRect;

    if (window->monitor)
    {
        GLFWvidmode mode;
        int xpos, ypos;

        _glfwPlatformGetVideoMode(window->monitor, &mode);
        _glfwPlatformGetMonitorPos(window->monitor, &xpos, &ypos);

        contentRect = NSMakeRect(xpos, ypos, mode.width, mode.height);
    }
    else
        contentRect = NSMakeRect(0, 0, wndconfig->width, wndconfig->height);

    window->ns.object = [[GLFWWindow alloc]
        initWithGlfwWindow:contentRect
                  styleMask:getStyleMask(window)
                    backing:NSBackingStoreBuffered
                 initWindow:window
    ];

    if (window->ns.object == nil)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create window");
        return false;
    }

    if (window->monitor)
        [window->ns.object setLevel:NSMainMenuWindowLevel + 1];
    else
    {
        [(NSWindow*) window->ns.object center];
        _glfw.ns.cascadePoint =
            NSPointToCGPoint([window->ns.object cascadeTopLeftFromPoint:
                              NSPointFromCGPoint(_glfw.ns.cascadePoint)]);

        if (wndconfig->resizable)
        {
            const NSWindowCollectionBehavior behavior =
                NSWindowCollectionBehaviorFullScreenPrimary |
                NSWindowCollectionBehaviorManaged;
            [window->ns.object setCollectionBehavior:behavior];
        }

        if (wndconfig->floating)
            [window->ns.object setLevel:NSFloatingWindowLevel];

        if (wndconfig->maximized)
            [window->ns.object zoom:nil];
    }

    if (strlen(wndconfig->ns.frameName))
        [window->ns.object setFrameAutosaveName:@(wndconfig->ns.frameName)];

    window->ns.view = [[GLFWContentView alloc] initWithGlfwWindow:window];
    window->ns.retina = wndconfig->ns.retina;

    if (fbconfig->transparent)
    {
        [window->ns.object setOpaque:NO];
        [window->ns.object setHasShadow:NO];
        [window->ns.object setBackgroundColor:[NSColor clearColor]];
    }

    [window->ns.object setContentView:window->ns.view];
    [window->ns.object makeFirstResponder:window->ns.view];
    [window->ns.object setTitle:@(wndconfig->title)];
    [window->ns.object setDelegate:window->ns.delegate];
    [window->ns.object setAcceptsMouseMovedEvents:YES];
    [window->ns.object setRestorable:NO];

    _glfwPlatformGetWindowSize(window, &window->ns.width, &window->ns.height);
    _glfwPlatformGetFramebufferSize(window, &window->ns.fbWidth, &window->ns.fbHeight);

    return true;
}


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

int _glfwPlatformCreateWindow(_GLFWwindow* window,
                              const _GLFWwndconfig* wndconfig,
                              const _GLFWctxconfig* ctxconfig,
                              const _GLFWfbconfig* fbconfig)
{
    window->ns.deadKeyState = 0;
    if (!_glfw.ns.finishedLaunching)
    {
        [NSApp run];
        _glfw.ns.finishedLaunching = true;
    }

    if (!createNativeWindow(window, wndconfig, fbconfig))
        return false;
    switch((GlfwCocoaColorSpaces)wndconfig->ns.color_space) {
        case SRGB_COLORSPACE: [window->ns.object setColorSpace:[NSColorSpace sRGBColorSpace]]; break;
        case DISPLAY_P3_COLORSPACE: [window->ns.object setColorSpace:[NSColorSpace displayP3ColorSpace]]; break;
        case DEFAULT_COLORSPACE: break;
    }

    if (ctxconfig->client != GLFW_NO_API)
    {
        if (ctxconfig->source == GLFW_NATIVE_CONTEXT_API)
        {
            if (!_glfwInitNSGL())
                return false;
            if (!_glfwCreateContextNSGL(window, ctxconfig, fbconfig))
                return false;
        }
        else if (ctxconfig->source == GLFW_EGL_CONTEXT_API)
        {
            // EGL implementation on macOS use CALayer* EGLNativeWindowType so we
            // need to get the layer for EGL window surface creation.
            [window->ns.view setWantsLayer:YES];
            window->ns.layer = [window->ns.view layer];

            if (!_glfwInitEGL())
                return false;
            if (!_glfwCreateContextEGL(window, ctxconfig, fbconfig))
                return false;
        }
        else if (ctxconfig->source == GLFW_OSMESA_CONTEXT_API)
        {
            if (!_glfwInitOSMesa())
                return false;
            if (!_glfwCreateContextOSMesa(window, ctxconfig, fbconfig))
                return false;
        }
    }

    if (window->monitor)
    {
        _glfwPlatformShowWindow(window);
        _glfwPlatformFocusWindow(window);
        acquireMonitor(window);
    }

    return true;
}

void _glfwPlatformDestroyWindow(_GLFWwindow* window)
{
    if (_glfw.ns.disabledCursorWindow == window)
        _glfw.ns.disabledCursorWindow = NULL;

    [window->ns.object orderOut:nil];

    if (window->monitor)
        releaseMonitor(window);

    if (window->context.destroy)
        window->context.destroy(window);

    [window->ns.object setDelegate:nil];
    [window->ns.delegate release];
    window->ns.delegate = nil;

    [window->ns.view removeGLFWWindow];
    [window->ns.view release];
    window->ns.view = nil;

    [window->ns.object removeGLFWWindow];
    [window->ns.object close];
    window->ns.object = nil;
}

void _glfwPlatformSetWindowTitle(_GLFWwindow* window UNUSED, const char* title)
{
    NSString* string = @(title);
    [window->ns.object setTitle:string];
    // HACK: Set the miniwindow title explicitly as setTitle: doesn't update it
    //       if the window lacks NSWindowStyleMaskTitled
    [window->ns.object setMiniwindowTitle:string];
}

void _glfwPlatformSetWindowIcon(_GLFWwindow* window UNUSED,
                                int count UNUSED, const GLFWimage* images UNUSED)
{
    _glfwInputError(GLFW_FEATURE_UNAVAILABLE,
                    "Cocoa: Regular windows do not have icons on macOS");
}

void _glfwPlatformGetWindowPos(_GLFWwindow* window, int* xpos, int* ypos)
{
    const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);

    if (xpos)
        *xpos = (int)contentRect.origin.x;
    if (ypos)
        *ypos = (int)_glfwTransformYNS(contentRect.origin.y + contentRect.size.height - 1);
}

void _glfwPlatformSetWindowPos(_GLFWwindow* window, int x, int y)
{
    const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
    const NSRect dummyRect = NSMakeRect(x, _glfwTransformYNS(y + contentRect.size.height - 1), 0, 0);
    const NSRect frameRect = [window->ns.object frameRectForContentRect:dummyRect];
    [window->ns.object setFrameOrigin:frameRect.origin];
}

void _glfwPlatformGetWindowSize(_GLFWwindow* window, int* width, int* height)
{
    const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);

    if (width)
        *width = (int)contentRect.size.width;
    if (height)
        *height = (int)contentRect.size.height;
}

void _glfwPlatformSetWindowSize(_GLFWwindow* window, int width, int height)
{
    if (window->monitor)
    {
        if (window->monitor->window == window)
            acquireMonitor(window);
    }
    else
    {
        // Disable window resizing in fullscreen.
        if ([window->ns.object styleMask] & NSWindowStyleMaskFullScreen || window->ns.in_traditional_fullscreen) return;
        NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
        contentRect.origin.y += contentRect.size.height - height;
        contentRect.size = NSMakeSize(width, height);
        [window->ns.object setFrame:[window->ns.object frameRectForContentRect:contentRect]
                            display:YES];
    }
}

void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window,
                                      int minwidth, int minheight,
                                      int maxwidth, int maxheight)
{
    if (minwidth == GLFW_DONT_CARE || minheight == GLFW_DONT_CARE)
        [window->ns.object setContentMinSize:NSMakeSize(0, 0)];
    else
        [window->ns.object setContentMinSize:NSMakeSize(minwidth, minheight)];

    if (maxwidth == GLFW_DONT_CARE || maxheight == GLFW_DONT_CARE)
        [window->ns.object setContentMaxSize:NSMakeSize(DBL_MAX, DBL_MAX)];
    else
        [window->ns.object setContentMaxSize:NSMakeSize(maxwidth, maxheight)];
}

void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom)
{
    if (numer == GLFW_DONT_CARE || denom == GLFW_DONT_CARE)
        [window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
    else
        [window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)];
}

void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr)
{
    if (widthincr != GLFW_DONT_CARE && heightincr != GLFW_DONT_CARE) {
        float xscale = 1, yscale = 1;
        _glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
        [window->ns.object setResizeIncrements:NSMakeSize(widthincr / xscale, heightincr / yscale)];
    } else {
        [window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
    }
}

void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height)
{
    const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
    const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect];

    if (width)
        *width = (int) fbRect.size.width;
    if (height)
        *height = (int) fbRect.size.height;
}

void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
                                     int* left, int* top,
                                     int* right, int* bottom)
{
    const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
    const NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect];

    if (left)
        *left = (int)(contentRect.origin.x - frameRect.origin.x);
    if (top)
        *top = (int)(frameRect.origin.y + frameRect.size.height -
               contentRect.origin.y - contentRect.size.height);
    if (right)
        *right = (int)(frameRect.origin.x + frameRect.size.width -
                 contentRect.origin.x - contentRect.size.width);
    if (bottom)
        *bottom = (int)(contentRect.origin.y - frameRect.origin.y);
}

void _glfwPlatformGetWindowContentScale(_GLFWwindow* window,
                                        float* xscale, float* yscale)
{
    const NSRect points = get_window_size_without_border_in_logical_pixels(window);
    const NSRect pixels = [window->ns.view convertRectToBacking:points];

    if (xscale)
        *xscale = (float) (pixels.size.width / points.size.width);
    if (yscale)
        *yscale = (float) (pixels.size.height / points.size.height);
}

monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
{
    return s_double_to_monotonic_t([NSEvent doubleClickInterval]);
}

void _glfwPlatformIconifyWindow(_GLFWwindow* window)
{
    [window->ns.object miniaturize:nil];
}

void _glfwPlatformRestoreWindow(_GLFWwindow* window)
{
    if ([window->ns.object isMiniaturized])
        [window->ns.object deminiaturize:nil];
    else if ([window->ns.object isZoomed])
        [window->ns.object zoom:nil];
}

void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
{
    if (![window->ns.object isZoomed])
        [window->ns.object zoom:nil];
}

void _glfwPlatformShowWindow(_GLFWwindow* window)
{
    [window->ns.object orderFront:nil];
}

void _glfwPlatformHideWindow(_GLFWwindow* window)
{
    [window->ns.object orderOut:nil];
}

void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED)
{
    [NSApp requestUserAttention:NSInformationalRequest];
}

int _glfwPlatformWindowBell(_GLFWwindow* window UNUSED)
{
    NSBeep();
    return true;
}

void _glfwPlatformFocusWindow(_GLFWwindow* window)
{
    // Make us the active application
    // HACK: This is here to prevent applications using only hidden windows from
    //       being activated, but should probably not be done every time any
    //       window is shown
    [NSApp activateIgnoringOtherApps:YES];
    [window->ns.object makeKeyAndOrderFront:nil];
}

void _glfwPlatformSetWindowMonitor(_GLFWwindow* window,
                                   _GLFWmonitor* monitor,
                                   int xpos, int ypos,
                                   int width, int height,
                                   int refreshRate UNUSED)
{
    if (window->monitor == monitor)
    {
        if (monitor)
        {
            if (monitor->window == window)
                acquireMonitor(window);
        }
        else
        {
            const NSRect contentRect =
                NSMakeRect(xpos, _glfwTransformYNS(ypos + height - 1), width, height);
            const NSRect frameRect =
                [window->ns.object frameRectForContentRect:contentRect
                                                 styleMask:getStyleMask(window)];

            [window->ns.object setFrame:frameRect display:YES];
        }

        return;
    }

    if (window->monitor)
        releaseMonitor(window);

    _glfwInputWindowMonitor(window, monitor);

    const NSUInteger styleMask = getStyleMask(window);
    [window->ns.object setStyleMask:styleMask];
    // HACK: Changing the style mask can cause the first responder to be cleared
    [window->ns.object makeFirstResponder:window->ns.view];

    if (window->monitor)
    {
        [window->ns.object setLevel:NSMainMenuWindowLevel + 1];
        [window->ns.object setHasShadow:NO];

        acquireMonitor(window);
    }
    else
    {
        NSRect contentRect = NSMakeRect(xpos, _glfwTransformYNS(ypos + height - 1),
                                        width, height);
        NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect
                                                            styleMask:styleMask];
        [window->ns.object setFrame:frameRect display:YES];

        if (window->numer != GLFW_DONT_CARE &&
            window->denom != GLFW_DONT_CARE)
        {
            [window->ns.object setContentAspectRatio:NSMakeSize(window->numer,
                                                                window->denom)];
        }

        if (window->minwidth != GLFW_DONT_CARE &&
            window->minheight != GLFW_DONT_CARE)
        {
            [window->ns.object setContentMinSize:NSMakeSize(window->minwidth,
                                                            window->minheight)];
        }

        if (window->maxwidth != GLFW_DONT_CARE &&
            window->maxheight != GLFW_DONT_CARE)
        {
            [window->ns.object setContentMaxSize:NSMakeSize(window->maxwidth,
                                                            window->maxheight)];
        }

        if (window->floating)
            [window->ns.object setLevel:NSFloatingWindowLevel];
        else
            [window->ns.object setLevel:NSNormalWindowLevel];

        [window->ns.object setHasShadow:YES];
        // HACK: Clearing NSWindowStyleMaskTitled resets and disables the window
        //       title property but the miniwindow title property is unaffected
        [window->ns.object setTitle:[window->ns.object miniwindowTitle]];
    }
}

int _glfwPlatformWindowFocused(_GLFWwindow* window)
{
    return [window->ns.object isKeyWindow];
}

int _glfwPlatformWindowOccluded(_GLFWwindow* window)
{
    return !([window->ns.object occlusionState] & NSWindowOcclusionStateVisible);
}

int _glfwPlatformWindowIconified(_GLFWwindow* window)
{
    return [window->ns.object isMiniaturized];
}

int _glfwPlatformWindowVisible(_GLFWwindow* window)
{
    return [window->ns.object isVisible];
}

int _glfwPlatformWindowMaximized(_GLFWwindow* window)
{
    return [window->ns.object isZoomed];
}

int _glfwPlatformWindowHovered(_GLFWwindow* window)
{
    const NSPoint point = [NSEvent mouseLocation];

    if ([NSWindow windowNumberAtPoint:point belowWindowWithWindowNumber:0] !=
        [window->ns.object windowNumber])
    {
        return false;
    }

    return NSMouseInRect(point,
        [window->ns.object convertRectToScreen:[window->ns.view frame]], NO);
}

int _glfwPlatformFramebufferTransparent(_GLFWwindow* window)
{
    return ![window->ns.object isOpaque] && ![window->ns.view isOpaque];
}

void _glfwPlatformSetWindowResizable(_GLFWwindow* window, bool enabled UNUSED)
{
    [window->ns.object setStyleMask:getStyleMask(window)];
}

void _glfwPlatformSetWindowDecorated(_GLFWwindow* window, bool enabled UNUSED)
{
    [window->ns.object setStyleMask:getStyleMask(window)];
    [window->ns.object makeFirstResponder:window->ns.view];
}

void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
{
    if (enabled)
        [window->ns.object setLevel:NSFloatingWindowLevel];
    else
        [window->ns.object setLevel:NSNormalWindowLevel];
}

void _glfwPlatformSetWindowMousePassthrough(_GLFWwindow* window, bool enabled)
{
    [window->ns.object setIgnoresMouseEvents:enabled];
}

float _glfwPlatformGetWindowOpacity(_GLFWwindow* window)
{
    return (float) [window->ns.object alphaValue];
}

void _glfwPlatformSetWindowOpacity(_GLFWwindow* window, float opacity)
{
    [window->ns.object setAlphaValue:opacity];
}

void _glfwPlatformSetRawMouseMotion(_GLFWwindow *window UNUSED, bool enabled UNUSED)
{
    _glfwInputError(GLFW_FEATURE_UNIMPLEMENTED,
                    "Cocoa: Raw mouse motion not yet implemented");
}

bool _glfwPlatformRawMouseMotionSupported(void)
{
    return false;
}

void
_glfwDispatchRenderFrame(CGDirectDisplayID displayID) {
    _GLFWwindow *w = _glfw.windowListHead;
    while (w) {
        if (w->ns.renderFrameRequested && displayID == displayIDForWindow(w)) {
            w->ns.renderFrameRequested = false;
            w->ns.renderFrameCallback((GLFWwindow*)w);
        }
        w = w->next;
    }
    for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
        _GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
        if (dl->displayID == displayID) {
            dl->first_unserviced_render_frame_request_at = 0;
        }
    }
}

void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos)
{
    const NSRect contentRect = [window->ns.view frame];
    // NOTE: The returned location uses base 0,1 not 0,0
    const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];

    if (xpos)
        *xpos = pos.x;
    if (ypos)
        *ypos = contentRect.size.height - pos.y;
}

void _glfwPlatformSetCursorPos(_GLFWwindow* window, double x, double y)
{
    updateCursorImage(window);

    const NSRect contentRect = [window->ns.view frame];
    // NOTE: The returned location uses base 0,1 not 0,0
    const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];

    window->ns.cursorWarpDeltaX += x - pos.x;
    window->ns.cursorWarpDeltaY += y - contentRect.size.height + pos.y;

    if (window->monitor)
    {
        CGDisplayMoveCursorToPoint(window->monitor->ns.displayID,
                                   CGPointMake(x, y));
    }
    else
    {
        const NSRect localRect = NSMakeRect(x, contentRect.size.height - y - 1, 0, 0);
        const NSRect globalRect = [window->ns.object convertRectToScreen:localRect];
        const NSPoint globalPoint = globalRect.origin;

        CGWarpMouseCursorPosition(CGPointMake(globalPoint.x,
                                              _glfwTransformYNS(globalPoint.y)));
    }
}

void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode UNUSED)
{
    if (_glfwPlatformWindowFocused(window))
        updateCursorMode(window);
}

const char* _glfwPlatformGetNativeKeyName(int keycode)
{
    UInt32 deadKeyState = 0;
    UniChar characters[8];
    UniCharCount characterCount = 0;

    if (UCKeyTranslate([(NSData*) _glfw.ns.unicodeData bytes],
                       keycode,
                       kUCKeyActionDisplay,
                       0,
                       LMGetKbdType(),
                       kUCKeyTranslateNoDeadKeysBit,
                       &deadKeyState,
                       sizeof(characters) / sizeof(characters[0]),
                       &characterCount,
                       characters) != noErr)
    {
        return NULL;
    }

    if (!characterCount)
        return NULL;

    convert_utf16_to_utf8(characters, characterCount, _glfw.ns.keyName, sizeof(_glfw.ns.keyName));
    return _glfw.ns.keyName;
}

int _glfwPlatformGetNativeKeyForKey(uint32_t glfw_key)
{
    if (GLFW_FKEY_FIRST <= glfw_key && glfw_key <= GLFW_FKEY_LAST) { // {{{
        switch(glfw_key) {
            /* start functional to macu (auto generated by gen-key-constants.py do not edit) */
        case GLFW_FKEY_ENTER: return NSCarriageReturnCharacter;
        case GLFW_FKEY_TAB: return NSTabCharacter;
        case GLFW_FKEY_BACKSPACE: return NSBackspaceCharacter;
        case GLFW_FKEY_INSERT: return NSInsertFunctionKey;
        case GLFW_FKEY_DELETE: return NSDeleteFunctionKey;
        case GLFW_FKEY_LEFT: return NSLeftArrowFunctionKey;
        case GLFW_FKEY_RIGHT: return NSRightArrowFunctionKey;
        case GLFW_FKEY_UP: return NSUpArrowFunctionKey;
        case GLFW_FKEY_DOWN: return NSDownArrowFunctionKey;
        case GLFW_FKEY_PAGE_UP: return NSPageUpFunctionKey;
        case GLFW_FKEY_PAGE_DOWN: return NSPageDownFunctionKey;
        case GLFW_FKEY_HOME: return NSHomeFunctionKey;
        case GLFW_FKEY_END: return NSEndFunctionKey;
        case GLFW_FKEY_SCROLL_LOCK: return NSScrollLockFunctionKey;
        case GLFW_FKEY_NUM_LOCK: return NSClearLineFunctionKey;
        case GLFW_FKEY_PRINT_SCREEN: return NSPrintScreenFunctionKey;
        case GLFW_FKEY_PAUSE: return NSPauseFunctionKey;
        case GLFW_FKEY_MENU: return NSMenuFunctionKey;
        case GLFW_FKEY_F1: return NSF1FunctionKey;
        case GLFW_FKEY_F2: return NSF2FunctionKey;
        case GLFW_FKEY_F3: return NSF3FunctionKey;
        case GLFW_FKEY_F4: return NSF4FunctionKey;
        case GLFW_FKEY_F5: return NSF5FunctionKey;
        case GLFW_FKEY_F6: return NSF6FunctionKey;
        case GLFW_FKEY_F7: return NSF7FunctionKey;
        case GLFW_FKEY_F8: return NSF8FunctionKey;
        case GLFW_FKEY_F9: return NSF9FunctionKey;
        case GLFW_FKEY_F10: return NSF10FunctionKey;
        case GLFW_FKEY_F11: return NSF11FunctionKey;
        case GLFW_FKEY_F12: return NSF12FunctionKey;
        case GLFW_FKEY_F13: return NSF13FunctionKey;
        case GLFW_FKEY_F14: return NSF14FunctionKey;
        case GLFW_FKEY_F15: return NSF15FunctionKey;
        case GLFW_FKEY_F16: return NSF16FunctionKey;
        case GLFW_FKEY_F17: return NSF17FunctionKey;
        case GLFW_FKEY_F18: return NSF18FunctionKey;
        case GLFW_FKEY_F19: return NSF19FunctionKey;
        case GLFW_FKEY_F20: return NSF20FunctionKey;
        case GLFW_FKEY_F21: return NSF21FunctionKey;
        case GLFW_FKEY_F22: return NSF22FunctionKey;
        case GLFW_FKEY_F23: return NSF23FunctionKey;
        case GLFW_FKEY_F24: return NSF24FunctionKey;
        case GLFW_FKEY_F25: return NSF25FunctionKey;
        case GLFW_FKEY_F26: return NSF26FunctionKey;
        case GLFW_FKEY_F27: return NSF27FunctionKey;
        case GLFW_FKEY_F28: return NSF28FunctionKey;
        case GLFW_FKEY_F29: return NSF29FunctionKey;
        case GLFW_FKEY_F30: return NSF30FunctionKey;
        case GLFW_FKEY_F31: return NSF31FunctionKey;
        case GLFW_FKEY_F32: return NSF32FunctionKey;
        case GLFW_FKEY_F33: return NSF33FunctionKey;
        case GLFW_FKEY_F34: return NSF34FunctionKey;
        case GLFW_FKEY_F35: return NSF35FunctionKey;
        case GLFW_FKEY_KP_ENTER: return NSEnterCharacter;
/* end functional to macu */
            default:
                return 0;
        }
    } // }}}
    if (!is_pua_char(glfw_key)) return glfw_key;
    return 0;
}

int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
                              const GLFWimage* image,
                              int xhot, int yhot, int count)
{
    NSImage* native;
    NSBitmapImageRep* rep;

    native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
    if (native == nil)
        return false;

    for (int i = 0; i < count; i++) {
        const GLFWimage *src = image + i;
        rep = [[NSBitmapImageRep alloc]
            initWithBitmapDataPlanes:NULL
                        pixelsWide:src->width
                        pixelsHigh:src->height
                    bitsPerSample:8
                    samplesPerPixel:4
                            hasAlpha:YES
                            isPlanar:NO
                    colorSpaceName:NSCalibratedRGBColorSpace
                        bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
                        bytesPerRow:src->width * 4
                        bitsPerPixel:32];
        if (rep == nil) {
            [native release];
            return false;
        }

        memcpy([rep bitmapData], src->pixels, src->width * src->height * 4);
        [native addRepresentation:rep];
        [rep release];
    }

    cursor->ns.object = [[NSCursor alloc] initWithImage:native
                                                hotSpot:NSMakePoint(xhot, yhot)];

    [native release];
    if (cursor->ns.object == nil)
        return false;
    return true;
}

int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape)
{
#define C(name, val) case name: cursor->ns.object = [NSCursor val]; break;
#define U(name, val) case name: cursor->ns.object = [[NSCursor class] performSelector:@selector(val)]; break;
    switch(shape) {
        C(GLFW_ARROW_CURSOR, arrowCursor);
        C(GLFW_IBEAM_CURSOR, IBeamCursor);
        C(GLFW_CROSSHAIR_CURSOR, crosshairCursor);
        C(GLFW_HAND_CURSOR, pointingHandCursor);
        C(GLFW_HRESIZE_CURSOR, resizeLeftRightCursor);
        C(GLFW_VRESIZE_CURSOR, resizeUpDownCursor);
        U(GLFW_NW_RESIZE_CURSOR, _windowResizeNorthWestSouthEastCursor);
        U(GLFW_NE_RESIZE_CURSOR, _windowResizeNorthEastSouthWestCursor);
        U(GLFW_SW_RESIZE_CURSOR, _windowResizeNorthEastSouthWestCursor);
        U(GLFW_SE_RESIZE_CURSOR, _windowResizeNorthWestSouthEastCursor);
        case GLFW_INVALID_CURSOR:
            return false;
    }
#undef C
#undef U

    if (!cursor->ns.object)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to retrieve standard cursor");
        return false;
    }

    [cursor->ns.object retain];
    return true;
}

void _glfwPlatformDestroyCursor(_GLFWcursor* cursor)
{
    if (cursor->ns.object)
        [(NSCursor*) cursor->ns.object release];
}

void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED)
{
    if (cursorInContentArea(window))
        updateCursorImage(window);
}

bool _glfwPlatformIsFullscreen(_GLFWwindow* w, unsigned int flags) {
    NSWindow *window = w->ns.object;
    bool traditional = !(flags & 1);
    if (traditional) { if(@available(macOS 10.15.7, *)) return w->ns.in_traditional_fullscreen; }
    NSWindowStyleMask sm = [window styleMask];
    return sm & NSWindowStyleMaskFullScreen;
}

bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
    NSWindow *window = w->ns.object;
    bool made_fullscreen = true;
    bool traditional = !(flags & 1);
    NSWindowStyleMask sm = [window styleMask];
    if (traditional) {
        if (@available(macOS 10.15.7, *)) {
            // As of Big Turd NSWindowStyleMaskFullScreen is no longer useable
            // Also no longer compatible after a minor release of macOS 10.15.7
            if (!w->ns.in_traditional_fullscreen) {
                w->ns.pre_full_screen_style_mask = sm;
                [window setStyleMask: NSWindowStyleMaskBorderless];
                [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
                [window setFrame:[window.screen frame] display:YES];
                w->ns.in_traditional_fullscreen = true;
            } else {
                made_fullscreen = false;
                [window setStyleMask: w->ns.pre_full_screen_style_mask];
                [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
                w->ns.in_traditional_fullscreen = false;
            }
        } else {
            bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
            if (!(in_fullscreen)) {
                sm |= NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen;
                [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
            } else {
                made_fullscreen = false;
                sm &= ~(NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen);
                [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
            }
            [window setStyleMask: sm];
        }
        // Changing the style mask causes the first responder to be cleared
        [window makeFirstResponder:w->ns.view];
        // If the dock and menubar are hidden going from maximized to fullscreen doesnt change the window size
        // and macOS forgets to trigger windowDidResize, so call it ourselves
        NSNotification *notification = [NSNotification notificationWithName:NSWindowDidResizeNotification object:window];
        [w->ns.delegate performSelector:@selector(windowDidResize:) withObject:notification afterDelay:0];
    } else {
        bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
        if (in_fullscreen) made_fullscreen = false;
        [window toggleFullScreen: nil];
    }
    return made_fullscreen;
}

// Clipboard {{{

#define UTI_ROUNDTRIP_PREFIX @"uti-is-typical-apple-nih."

static NSString*
mime_to_uti(const char *mime) {
    if (strcmp(mime, "text/plain") == 0) return NSPasteboardTypeString;
    if (@available(macOS 11.0, *)) {
        UTType *t = [UTType typeWithMIMEType:@(mime)];  // auto-released
        if (t != nil && !t.dynamic) return t.identifier;
    }
    return [NSString stringWithFormat:@"%@%s", UTI_ROUNDTRIP_PREFIX, mime];  // auto-released
}

static const char*
uti_to_mime(NSString *uti) {
    if ([uti isEqualToString:NSPasteboardTypeString]) return "text/plain";
    if ([uti hasPrefix:UTI_ROUNDTRIP_PREFIX]) return [[uti substringFromIndex:[UTI_ROUNDTRIP_PREFIX length]] UTF8String];
    if (@available(macOS 11.0, *)) {
        UTType *t = [UTType typeWithIdentifier:uti];  // auto-released
        if (t.preferredMIMEType != nil) return [t.preferredMIMEType UTF8String];
    }
    return "";
}

static void
list_clipboard_mimetypes(GLFWclipboardwritedatafun write_data, void *object) {
#define w(x) { if (ok) ok = write_data(object, x, strlen(x)); }
    NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
    NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
    BOOL has_file_urls = [pasteboard canReadObjectForClasses:@[[NSURL class]] options:options];
    BOOL has_strings = [pasteboard canReadObjectForClasses:@[[NSString class]] options:nil];
    /* NSLog(@"has_file_urls: %d has_strings: %d", has_file_urls, has_strings); */
    bool ok = true;
    if (has_strings) w("text/plain");
    if (has_file_urls) w("text/local-path-list");
    for (NSPasteboardItem * item in pasteboard.pasteboardItems) {
        for (NSPasteboardType type in item.types) {
            /* NSLog(@"%@", type); */
            const char *mime = uti_to_mime(type);
            if (mime && mime[0] && ![@(mime) hasPrefix:@"text/plain"]) {
                /* NSLog(@"ut: %@ mt: %@ tags: %@", ut, ut.preferredMIMEType, ut.tags); */
                w(mime);
            }
        }
    }
#undef w
}

static void
get_text_plain(GLFWclipboardwritedatafun write_data, void *object) {
    NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
    NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
    NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]] options:options];
    bool found = false;
    if (objs) {
        const NSUInteger count = [objs count];
        if (count) {
            NSMutableData *path_list = [NSMutableData dataWithCapacity:4096];  // auto-released
            NSMutableData *text_list = [NSMutableData dataWithCapacity:4096];  // auto-released
            for (NSUInteger i = 0;  i < count;  i++) {
                id obj = objs[i];
                if ([obj isKindOfClass:[NSURL class]]) {
                    NSURL *url = (NSURL*)obj;
                    if (url.fileURL && url.fileSystemRepresentation) {
                        if ([path_list length] > 0) [path_list appendBytes:"\n" length:1];
                        [path_list appendBytes:url.fileSystemRepresentation length:strlen(url.fileSystemRepresentation)];
                    }
                } else if ([obj isKindOfClass:[NSString class]]) {
                    if ([text_list length] > 0) [text_list appendBytes:"\n" length:1];
                    [text_list appendData:[obj dataUsingEncoding:NSUTF8StringEncoding]];
                }
            }
            const NSMutableData *text = nil;
            if (path_list.length > 0) text = path_list;
            else if (text_list.length > 0) text = text_list;
            if (text) {
                found = true;
                write_data(object, text.mutableBytes, text.length);
            }
        }
    }
    if (!found) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve text/plain from pasteboard");
}

void
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
    if (clipboard_type != GLFW_CLIPBOARD) return;
    if (mime_type == NULL) {
        list_clipboard_mimetypes(write_data, object);
        return;
    }
    if (strcmp(mime_type, "text/plain") == 0) {
        get_text_plain(write_data, object);
        return;
    }
    NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
    /* NSLog(@"mime: %s uti: %@", mime_type, mime_to_uti(mime_type)); */
    NSPasteboardType t = [pasteboard availableTypeFromArray:@[mime_to_uti(mime_type)]];
    /* NSLog(@"available type: %@", t); */
    if (t != nil) {
        NSData *data = [pasteboard dataForType:t];  // auto-released
        /* NSLog(@"data: %@", data); */
        if (data != nil && data.length > 0) {
            write_data(object, data.bytes, data.length);
        }
    }
}

static NSMutableData*
get_clipboard_data(const _GLFWClipboardData *cd, const char *mime) {
    NSMutableData *ans = [NSMutableData dataWithCapacity:8192];
    if (ans == nil) return nil;
    GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
    void *iter = chunk.iter;
    if (!iter) return ans;
    while (true) {
        chunk = cd->get_data(mime, iter, cd->ctype);
        if (!chunk.sz) break;
        [ans appendBytes:chunk.data length:chunk.sz];
        if (chunk.free) chunk.free((void*)chunk.free_data);
    }
    cd->get_data(NULL, iter, cd->ctype);
    return ans;
}


void
_glfwPlatformSetClipboard(GLFWClipboardType t) {
    if (t != GLFW_CLIPBOARD) return;
    NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
    NSMutableArray<NSPasteboardType> *ptypes = [NSMutableArray arrayWithCapacity:_glfw.clipboard.num_mime_types];  // auto-released
    for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) {
        [ptypes addObject:mime_to_uti(_glfw.clipboard.mime_types[i])];
    }
    [pasteboard declareTypes:ptypes owner:nil];
    for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) {
        NSMutableData *data = get_clipboard_data(&_glfw.clipboard, _glfw.clipboard.mime_types[i]);  // auto-released
        /* NSLog(@"putting data: %@ for: %s with UTI: %@", data, _glfw.clipboard.mime_types[i], ptypes[i]); */
        if (data != nil) [pasteboard setData:data forType:ptypes[i]];
    }
}
// }}}

EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs)
{
    if (_glfw.egl.ANGLE_platform_angle)
    {
        int type = 0;

        if (_glfw.egl.ANGLE_platform_angle_opengl)
        {
            if (_glfw.hints.init.angleType == GLFW_ANGLE_PLATFORM_TYPE_OPENGL)
                type = EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE;
        }

        if (_glfw.egl.ANGLE_platform_angle_metal)
        {
            if (_glfw.hints.init.angleType == GLFW_ANGLE_PLATFORM_TYPE_METAL)
                type = EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE;
        }

        if (type)
        {
            *attribs = calloc(3, sizeof(EGLint));
            (*attribs)[0] = EGL_PLATFORM_ANGLE_TYPE_ANGLE;
            (*attribs)[1] = type;
            (*attribs)[2] = EGL_NONE;
            return EGL_PLATFORM_ANGLE_ANGLE;
        }
    }

    return 0;
}

EGLNativeDisplayType _glfwPlatformGetEGLNativeDisplay(void)
{
    return EGL_DEFAULT_DISPLAY;
}

EGLNativeWindowType _glfwPlatformGetEGLNativeWindow(_GLFWwindow* window)
{
    return window->ns.layer;
}

void _glfwPlatformGetRequiredInstanceExtensions(char** extensions)
{
    if (_glfw.vk.KHR_surface && _glfw.vk.EXT_metal_surface)
    {
        extensions[0] = "VK_KHR_surface";
        extensions[1] = "VK_EXT_metal_surface";
    }
    else if (_glfw.vk.KHR_surface && _glfw.vk.MVK_macos_surface)
    {
        extensions[0] = "VK_KHR_surface";
        extensions[1] = "VK_MVK_macos_surface";
    }
}

int _glfwPlatformGetPhysicalDevicePresentationSupport(VkInstance instance UNUSED,
                                                      VkPhysicalDevice device UNUSED,
                                                      uint32_t queuefamily UNUSED)
{
    return true;
}

VkResult _glfwPlatformCreateWindowSurface(VkInstance instance,
                                          _GLFWwindow* window,
                                          const VkAllocationCallbacks* allocator,
                                          VkSurfaceKHR* surface)
{
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101100
    // HACK: Dynamically load Core Animation to avoid adding an extra
    //       dependency for the majority who don't use MoltenVK
    NSBundle* bundle = [NSBundle bundleWithPath:@"/System/Library/Frameworks/QuartzCore.framework"];
    if (!bundle)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to find QuartzCore.framework");
        return VK_ERROR_EXTENSION_NOT_PRESENT;
    }

    // NOTE: Create the layer here as makeBackingLayer should not return nil
    window->ns.layer = [[bundle classNamed:@"CAMetalLayer"] layer];
    if (!window->ns.layer)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to create layer for view");
        return VK_ERROR_EXTENSION_NOT_PRESENT;
    }

    if (window->ns.retina)
        [window->ns.layer setContentsScale:[window->ns.object backingScaleFactor]];

    [window->ns.view setLayer:window->ns.layer];
    [window->ns.view setWantsLayer:YES];

    VkResult err;

    if (_glfw.vk.EXT_metal_surface)
    {
        VkMetalSurfaceCreateInfoEXT sci;

        PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT;
        vkCreateMetalSurfaceEXT = (PFN_vkCreateMetalSurfaceEXT)
            vkGetInstanceProcAddr(instance, "vkCreateMetalSurfaceEXT");
        if (!vkCreateMetalSurfaceEXT)
        {
            _glfwInputError(GLFW_API_UNAVAILABLE,
                            "Cocoa: Vulkan instance missing VK_EXT_metal_surface extension");
            return VK_ERROR_EXTENSION_NOT_PRESENT;
        }

        memset(&sci, 0, sizeof(sci));
        sci.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT;
        sci.pLayer = window->ns.layer;

        err = vkCreateMetalSurfaceEXT(instance, &sci, allocator, surface);
    }
    else
    {
        VkMacOSSurfaceCreateInfoMVK sci;

        PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK;
        vkCreateMacOSSurfaceMVK = (PFN_vkCreateMacOSSurfaceMVK)
            vkGetInstanceProcAddr(instance, "vkCreateMacOSSurfaceMVK");
        if (!vkCreateMacOSSurfaceMVK)
        {
            _glfwInputError(GLFW_API_UNAVAILABLE,
                            "Cocoa: Vulkan instance missing VK_MVK_macos_surface extension");
            return VK_ERROR_EXTENSION_NOT_PRESENT;
        }

        memset(&sci, 0, sizeof(sci));
        sci.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK;
        sci.pView = window->ns.view;

        err = vkCreateMacOSSurfaceMVK(instance, &sci, allocator, surface);
    }

    if (err)
    {
        _glfwInputError(GLFW_PLATFORM_ERROR,
                        "Cocoa: Failed to create Vulkan surface: %s",
                        _glfwGetVulkanResultString(err));
    }

    return err;
#else
    return VK_ERROR_EXTENSION_NOT_PRESENT;
#endif
}


//////////////////////////////////////////////////////////////////////////
//////                        GLFW native API                       //////
//////////////////////////////////////////////////////////////////////////

GLFWAPI id glfwGetCocoaWindow(GLFWwindow* handle)
{
    _GLFWwindow* window = (_GLFWwindow*) handle;
    _GLFW_REQUIRE_INIT_OR_RETURN(nil);
    return window->ns.object;
}

GLFWAPI void glfwHideCocoaTitlebar(GLFWwindow* handle, bool yes) {
    @autoreleasepool {
    _GLFWwindow* w = (_GLFWwindow*) handle;
    NSWindow *window = w->ns.object;
    w->ns.titlebar_hidden = yes;
    NSButton *button;
    button = [window standardWindowButton: NSWindowCloseButton];
    if (button) button.hidden = yes;
    button = [window standardWindowButton: NSWindowMiniaturizeButton];
    if (button) button.hidden = yes;
    button = [window standardWindowButton: NSWindowZoomButton];
    [window setTitlebarAppearsTransparent:yes];
    if (button) button.hidden = yes;
    if (yes) {
        [window setTitleVisibility:NSWindowTitleHidden];
        [window setStyleMask: [window styleMask] | NSWindowStyleMaskFullSizeContentView];
    } else {
        [window setTitleVisibility:NSWindowTitleVisible];
        [window setStyleMask: [window styleMask] & ~NSWindowStyleMaskFullSizeContentView];
    }
    } // autoreleasepool
}

GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *handle, GLFWcocoatextinputfilterfun callback) {
    _GLFWwindow* window = (_GLFWwindow*) handle;
    _GLFW_REQUIRE_INIT_OR_RETURN(nil);
    GLFWcocoatextinputfilterfun previous = window->ns.textInputFilterCallback;
    window->ns.textInputFilterCallback = callback;
    return previous;
}

GLFWAPI GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback) {
    _GLFW_REQUIRE_INIT_OR_RETURN(nil);
    GLFWhandleurlopen prev = _glfw.ns.url_open_callback;
    _glfw.ns.url_open_callback = callback;
    return prev;
}

GLFWAPI GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *handle, GLFWcocoatogglefullscreenfun callback) {
    _GLFWwindow* window = (_GLFWwindow*) handle;
    _GLFW_REQUIRE_INIT_OR_RETURN(nil);
    GLFWcocoatogglefullscreenfun previous = window->ns.toggleFullscreenCallback;
    window->ns.toggleFullscreenCallback = callback;
    return previous;
}

GLFWAPI void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun callback) {
    requestRenderFrame((_GLFWwindow*)w, callback);
}

GLFWAPI uint32_t
glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int *cocoa_mods) {
    *cocoa_mods = 0;
    if (glfw_mods & GLFW_MOD_SHIFT)
        *cocoa_mods |= NSEventModifierFlagShift;
    if (glfw_mods & GLFW_MOD_CONTROL)
        *cocoa_mods |= NSEventModifierFlagControl;
    if (glfw_mods & GLFW_MOD_ALT)
        *cocoa_mods |= NSEventModifierFlagOption;
    if (glfw_mods & GLFW_MOD_SUPER)
        *cocoa_mods |= NSEventModifierFlagCommand;
    if (glfw_mods & GLFW_MOD_CAPS_LOCK)
        *cocoa_mods |= NSEventModifierFlagCapsLock;
    return _glfwPlatformGetNativeKeyForKey(glfw_key);
}


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

// Transforms a y-coordinate between the CG display and NS screen spaces
//
float _glfwTransformYNS(float y)
{
    return CGDisplayBounds(CGMainDisplayID()).size.height - y - 1;
}

void _glfwCocoaPostEmptyEvent(void) {
    NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
                                        location:NSMakePoint(0, 0)
                                   modifierFlags:0
                                       timestamp:0
                                    windowNumber:0
                                         context:nil
                                         subtype:0
                                           data1:0
                                           data2:0];
    [NSApp postEvent:event atStart:YES];
}
