//! Application runtime implementation that uses GLFW (https://www.glfw.org/).
//!
//! This works on macOS and Linux with OpenGL and Metal.
//! (The above sentence may be out of date).

const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const glfw = @import("glfw");
const macos = @import("macos");
const objc = @import("objc");
const cli = @import("../cli.zig");
const input = @import("../input.zig");
const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const Renderer = renderer.Renderer;
const apprt = @import("../apprt.zig");
const CoreApp = @import("../App.zig");
const CoreSurface = @import("../Surface.zig");
const configpkg = @import("../config.zig");
const Config = @import("../config.zig").Config;

// Get native API access on certain platforms so we can do more customization.
const glfwNative = glfw.Native(.{
    .cocoa = builtin.target.isDarwin(),
    .x11 = builtin.os.tag == .linux,
});

const log = std.log.scoped(.glfw);

pub const App = struct {
    app: *CoreApp,
    config: Config,

    /// Flips to true to quit on the next event loop tick. This
    /// never goes false and forces the event loop to exit.
    quit: bool = false,

    /// Mac-specific state.
    darwin: if (Darwin.enabled) Darwin else void,

    pub const Options = struct {};

    pub fn init(core_app: *CoreApp, _: Options) !App {
        if (comptime builtin.target.isDarwin()) {
            log.warn("WARNING WARNING WARNING: GLFW ON MAC HAS BUGS.", .{});
            log.warn("You should use the AppKit-based app instead. The official download", .{});
            log.warn("is properly built and available from GitHub. If you're building from", .{});
            log.warn("source, see the README for details on how to build the AppKit app.", .{});
        }

        if (!glfw.init(.{})) {
            if (glfw.getError()) |err| {
                log.err("error initializing GLFW err={} msg={s}", .{
                    err.error_code,
                    err.description,
                });
                return err.error_code;
            }

            return error.GlfwInitFailedUnknownReason;
        }
        glfw.setErrorCallback(glfwErrorCallback);

        // Mac-specific state. For example, on Mac we enable window tabbing.
        var darwin = if (Darwin.enabled) try Darwin.init() else {};
        errdefer if (Darwin.enabled) darwin.deinit();

        // Load our configuration
        var config = try Config.load(core_app.alloc);
        errdefer config.deinit();

        // If we had configuration errors, then log them.
        if (!config._diagnostics.empty()) {
            var buf = std.ArrayList(u8).init(core_app.alloc);
            defer buf.deinit();
            for (config._diagnostics.items()) |diag| {
                try diag.write(buf.writer());
                log.warn("configuration error: {s}", .{buf.items});
                buf.clearRetainingCapacity();
            }

            // If we have any CLI errors, exit.
            if (config._diagnostics.containsLocation(.cli)) {
                log.warn("CLI errors detected, exiting", .{});
                _ = core_app.mailbox.push(.{
                    .quit = {},
                }, .{ .forever = {} });
            }
        }

        // Queue a single new window that starts on launch
        // Note: above we may send a quit so this may never happen
        _ = core_app.mailbox.push(.{
            .new_window = .{},
        }, .{ .forever = {} });

        // We want the event loop to wake up instantly so we can process our tick.
        glfw.postEmptyEvent();

        return .{
            .app = core_app,
            .config = config,
            .darwin = darwin,
        };
    }

    pub fn terminate(self: *App) void {
        self.config.deinit();
        glfw.terminate();
    }

    /// Run the event loop. This doesn't return until the app exits.
    pub fn run(self: *App) !void {
        while (true) {
            // Wait for any events from the app event loop. wakeup will post
            // an empty event so that this will return.
            //
            // Warning: a known issue on macOS is that this will block while
            // a resize event is actively happening, which will prevent the
            // app tick from happening. I don't know know a way around this
            // but its not a big deal since we don't use glfw for the official
            // mac app, but noting it in case anyone builds for macos using
            // glfw.
            glfw.waitEvents();

            // Tick the terminal app
            try self.app.tick(self);

            // If the tick caused us to quit, then we're done.
            if (self.quit or self.app.surfaces.items.len == 0) {
                for (self.app.surfaces.items) |surface| {
                    surface.close(false);
                }

                return;
            }
        }
    }

    /// Wakeup the event loop. This should be able to be called from any thread.
    pub fn wakeup(self: *const App) void {
        _ = self;
        glfw.postEmptyEvent();
    }

    /// Perform a given action. Returns `true` if the action was able to be
    /// performed, `false` otherwise.
    pub fn performAction(
        self: *App,
        target: apprt.Target,
        comptime action: apprt.Action.Key,
        value: apprt.Action.Value(action),
    ) !bool {
        switch (action) {
            .quit => self.quit = true,

            .new_window => _ = try self.newSurface(switch (target) {
                .app => null,
                .surface => |v| v,
            }),

            .new_tab => try self.newTab(switch (target) {
                .app => null,
                .surface => |v| v,
            }),

            .size_limit => switch (target) {
                .app => {},
                .surface => |surface| try surface.rt_surface.setSizeLimits(.{
                    .width = value.min_width,
                    .height = value.min_height,
                }, if (value.max_width > 0) .{
                    .width = value.max_width,
                    .height = value.max_height,
                } else null),
            },

            .initial_size => switch (target) {
                .app => {},
                .surface => |surface| try surface.rt_surface.setInitialWindowSize(
                    value.width,
                    value.height,
                ),
            },

            .toggle_fullscreen => self.toggleFullscreen(target),

            .open_config => try configpkg.edit.open(self.app.alloc),

            .set_title => switch (target) {
                .app => {},
                .surface => |surface| try surface.rt_surface.setTitle(value.title),
            },

            .mouse_shape => switch (target) {
                .app => {},
                .surface => |surface| try surface.rt_surface.setMouseShape(value),
            },

            .mouse_visibility => switch (target) {
                .app => {},
                .surface => |surface| surface.rt_surface.setMouseVisibility(switch (value) {
                    .visible => true,
                    .hidden => false,
                }),
            },

            .reload_config => try self.reloadConfig(target, value),

            // Unimplemented
            .new_split,
            .goto_split,
            .resize_split,
            .equalize_splits,
            .toggle_split_zoom,
            .present_terminal,
            .close_all_windows,
            .close_tab,
            .toggle_tab_overview,
            .toggle_window_decorations,
            .toggle_quick_terminal,
            .toggle_visibility,
            .goto_tab,
            .move_tab,
            .inspector,
            .render_inspector,
            .quit_timer,
            .secure_input,
            .key_sequence,
            .desktop_notification,
            .mouse_over_link,
            .cell_size,
            .renderer_health,
            .color_change,
            .pwd,
            .config_change,
            .toggle_maximize,
            => {
                log.info("unimplemented action={}", .{action});
                return false;
            },
        }

        return true;
    }

    /// Reload the configuration. This should return the new configuration.
    /// The old value can be freed immediately at this point assuming a
    /// successful return.
    ///
    /// The returned pointer value is only valid for a stable self pointer.
    fn reloadConfig(
        self: *App,
        target: apprt.action.Target,
        opts: apprt.action.ReloadConfig,
    ) !void {
        if (opts.soft) {
            switch (target) {
                .app => try self.app.updateConfig(self, &self.config),
                .surface => |core_surface| try core_surface.updateConfig(
                    &self.config,
                ),
            }
            return;
        }

        // Load our configuration
        var config = try Config.load(self.app.alloc);
        errdefer config.deinit();

        // Call into our app to update
        switch (target) {
            .app => try self.app.updateConfig(self, &config),
            .surface => |core_surface| try core_surface.updateConfig(&config),
        }

        // Update the existing config, be sure to clean up the old one.
        self.config.deinit();
        self.config = config;
    }

    /// Toggle the window to fullscreen mode.
    fn toggleFullscreen(self: *App, target: apprt.Target) void {
        _ = self;
        const surface: *Surface = switch (target) {
            .app => return,
            .surface => |v| v.rt_surface,
        };
        const win = surface.window;

        if (surface.isFullscreen()) {
            win.setMonitor(
                null,
                @intCast(surface.monitor_dims.position_x),
                @intCast(surface.monitor_dims.position_y),
                surface.monitor_dims.width,
                surface.monitor_dims.height,
                0,
            );
            return;
        }

        const monitor = win.getMonitor() orelse monitor: {
            log.warn("window had null monitor, getting primary monitor", .{});
            break :monitor glfw.Monitor.getPrimary() orelse {
                log.warn("window could not get any monitor. will not perform action", .{});
                return;
            };
        };

        const video_mode = monitor.getVideoMode() orelse {
            log.warn("failed to get video mode. will not perform action", .{});
            return;
        };

        const position = win.getPos();
        const size = surface.getSize() catch {
            log.warn("failed to get window size. will not perform fullscreen action", .{});
            return;
        };

        surface.monitor_dims = .{
            .width = size.width,
            .height = size.height,
            .position_x = position.x,
            .position_y = position.y,
        };

        win.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), 0);
    }

    /// Create a new tab in the parent surface.
    fn newTab(self: *App, parent_: ?*CoreSurface) !void {
        if (!Darwin.enabled) {
            log.warn("tabbing is not supported on this platform", .{});
            return;
        }

        const parent = parent_ orelse {
            _ = try self.newSurface(null);
            return;
        };

        // Create the new window
        const window = try self.newSurface(parent);

        // Add the new window the parent window
        const parent_win = glfwNative.getCocoaWindow(parent.rt_surface.window).?;
        const other_win = glfwNative.getCocoaWindow(window.window).?;
        const NSWindowOrderingMode = enum(isize) { below = -1, out = 0, above = 1 };
        const nswindow = objc.Object.fromId(parent_win);
        nswindow.msgSend(void, objc.sel("addTabbedWindow:ordered:"), .{
            objc.Object.fromId(other_win),
            NSWindowOrderingMode.above,
        });

        // Adding a new tab can cause the tab bar to appear which changes
        // our viewport size. We need to call the size callback in order to
        // update values. For example, we need this to set the proper mouse selection
        // point in the grid.
        const size = parent.rt_surface.getSize() catch |err| {
            log.err("error querying window size for size callback on new tab err={}", .{err});
            return;
        };
        parent.sizeCallback(size) catch |err| {
            log.err("error in size callback from new tab err={}", .{err});
            return;
        };
    }

    fn newSurface(self: *App, parent_: ?*CoreSurface) !*Surface {
        // Grab a surface allocation because we're going to need it.
        var surface = try self.app.alloc.create(Surface);
        errdefer self.app.alloc.destroy(surface);

        // Create the surface -- because windows are surfaces for glfw.
        try surface.init(self);
        errdefer surface.deinit();

        // If we have a parent, inherit some properties
        if (self.config.@"window-inherit-font-size") {
            if (parent_) |parent| {
                try surface.core_surface.setFontSize(parent.font_size);
            }
        }

        return surface;
    }

    /// Close the given surface.
    pub fn closeSurface(self: *App, surface: *Surface) void {
        surface.deinit();
        self.app.alloc.destroy(surface);
    }

    pub fn redrawSurface(self: *App, surface: *Surface) void {
        _ = self;
        _ = surface;

        @panic("This should never be called for GLFW.");
    }

    pub fn redrawInspector(self: *App, surface: *Surface) void {
        _ = self;
        _ = surface;

        // GLFW doesn't support the inspector
    }

    fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void {
        std.log.warn("glfw error={} message={s}", .{ code, desc });

        // Workaround for: https://github.com/ocornut/imgui/issues/5908
        // If we get an invalid value with "scancode" in the message we assume
        // it is from the glfw key callback that imgui sets and we clear the
        // error so that our future code doesn't crash.
        if (code == glfw.ErrorCode.InvalidValue and
            std.mem.indexOf(u8, desc, "scancode") != null)
        {
            _ = glfw.getError();
        }
    }

    pub fn keyboardLayout(self: *const App) input.KeyboardLayout {
        _ = self;

        // Not supported by glfw
        return .unknown;
    }

    /// Mac-specific settings. This is only enabled when the target is
    /// Mac and the artifact is a standalone exe. We don't target libs because
    /// the embedded API doesn't do windowing.
    const Darwin = struct {
        const enabled = builtin.target.isDarwin() and build_config.artifact == .exe;

        tabbing_id: *macos.foundation.String,

        pub fn init() !Darwin {
            const NSWindow = objc.getClass("NSWindow").?;
            NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true});

            // Our tabbing ID allows all of our windows to group together
            const tabbing_id = try macos.foundation.String.createWithBytes(
                "com.mitchellh.ghostty.window",
                .utf8,
                false,
            );
            errdefer tabbing_id.release();

            // Setup our Mac settings
            return .{ .tabbing_id = tabbing_id };
        }

        pub fn deinit(self: *Darwin) void {
            self.tabbing_id.release();
            self.* = undefined;
        }
    };
};

/// These are used to keep track of the original monitor values so that we can
/// safely toggle on and off of fullscreen.
const MonitorDimensions = struct {
    width: u32,
    height: u32,
    position_x: i64,
    position_y: i64,
};

/// Surface represents the drawable surface for glfw. In glfw, a surface
/// is always a window because that is the only abstraction that glfw exposes.
///
/// This means that there is no way for the glfw runtime to support tabs,
/// splits, etc. without considerable effort. In fact, on Darwin, we do
/// support tabs because the minimal tabbing interface is a window abstraction,
/// but this is a bit of a hack. The native Swift runtime should be used instead
/// which uses real native tabbing.
///
/// Other runtimes a surface usually represents the equivalent of a "view"
/// or "widget" level granularity.
pub const Surface = struct {
    /// The glfw window handle
    window: glfw.Window,

    /// The glfw mouse cursor handle.
    cursor: ?glfw.Cursor,

    /// The app we're part of
    app: *App,

    /// A core surface
    core_surface: CoreSurface,

    /// This is the key event that was processed in keyCallback. This is only
    /// non-null if the event was NOT consumed in keyCallback. This lets us
    /// know in charCallback whether we should populate it and call it again.
    /// (GLFW guarantees that charCallback is called after keyCallback).
    key_event: ?input.KeyEvent = null,

    /// The monitor dimensions so we can toggle fullscreen on and off.
    monitor_dims: MonitorDimensions,

    /// Save the title text so that we can return it later when requested.
    /// This is allocated from the heap so it must be freed when we deinit the
    /// surface.
    title_text: ?[:0]const u8 = null,

    pub const Options = struct {};

    /// Initialize the surface into the given self pointer. This gives a
    /// stable pointer to the destination that can be used for callbacks.
    pub fn init(self: *Surface, app: *App) !void {
        // Create our window
        const win = glfw.Window.create(
            640,
            480,
            "ghostty",
            if (app.config.fullscreen) glfw.Monitor.getPrimary() else null,
            null,
            Renderer.glfwWindowHints(&app.config),
        ) orelse return glfw.mustGetErrorCode();
        errdefer win.destroy();

        // Setup our
        setInitialWindowPosition(
            win,
            app.config.@"window-position-x",
            app.config.@"window-position-y",
        );

        // Get our physical DPI - debug only because we don't have a use for
        // this but the logging of it may be useful
        if (builtin.mode == .Debug) {
            const monitor = win.getMonitor() orelse monitor: {
                log.warn("window had null monitor, getting primary monitor", .{});
                break :monitor glfw.Monitor.getPrimary().?;
            };
            const video_mode = monitor.getVideoMode() orelse return glfw.mustGetErrorCode();
            const physical_size = monitor.getPhysicalSize();
            const physical_x_dpi = @as(f32, @floatFromInt(video_mode.getWidth())) / (@as(f32, @floatFromInt(physical_size.width_mm)) / 25.4);
            const physical_y_dpi = @as(f32, @floatFromInt(video_mode.getHeight())) / (@as(f32, @floatFromInt(physical_size.height_mm)) / 25.4);
            log.debug("physical dpi x={} y={}", .{
                physical_x_dpi,
                physical_y_dpi,
            });
        }

        // On Mac, enable window tabbing
        if (App.Darwin.enabled) {
            const NSWindowTabbingMode = enum(usize) { automatic = 0, preferred = 1, disallowed = 2 };
            const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win).?);

            // Tabbing mode enables tabbing at all
            nswindow.setProperty("tabbingMode", NSWindowTabbingMode.automatic);

            // All windows within a tab bar must have a matching tabbing ID.
            // The app sets this up for us.
            nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id);
        }

        // Set our callbacks
        win.setUserPointer(&self.core_surface);
        win.setSizeCallback(sizeCallback);
        win.setCharCallback(charCallback);
        win.setKeyCallback(keyCallback);
        win.setFocusCallback(focusCallback);
        win.setRefreshCallback(refreshCallback);
        win.setScrollCallback(scrollCallback);
        win.setCursorPosCallback(cursorPosCallback);
        win.setMouseButtonCallback(mouseButtonCallback);
        win.setDropCallback(dropCallback);

        const dimensions: MonitorDimensions = dimensions: {
            const pos = win.getPos();
            const size = win.getFramebufferSize();
            break :dimensions .{
                .width = size.width,
                .height = size.height,
                .position_x = pos.x,
                .position_y = pos.y,
            };
        };

        // Build our result
        self.* = .{
            .app = app,
            .window = win,
            .cursor = null,
            .core_surface = undefined,
            .monitor_dims = dimensions,
        };
        errdefer self.* = undefined;

        // Initialize our cursor
        try self.setMouseShape(.text);

        // Add ourselves to the list of surfaces on the app.
        try app.app.addSurface(self);
        errdefer app.app.deleteSurface(self);

        // Get our new surface config
        var config = try apprt.surface.newConfig(app.app, &app.config);
        defer config.deinit();

        // Initialize our surface now that we have the stable pointer.
        try self.core_surface.init(
            app.app.alloc,
            &config,
            app.app,
            app,
            self,
        );
        errdefer self.core_surface.deinit();
    }

    pub fn deinit(self: *Surface) void {
        if (self.title_text) |t| self.core_surface.alloc.free(t);

        // Remove ourselves from the list of known surfaces in the app.
        self.app.app.deleteSurface(self);

        // Clean up our core surface so that all the rendering and IO stop.
        self.core_surface.deinit();

        if (App.Darwin.enabled) {
            const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?);
            const tabgroup = nswindow.getProperty(objc.Object, "tabGroup");
            const windows = tabgroup.getProperty(objc.Object, "windows");
            switch (windows.getProperty(usize, "count")) {
                // If we're going down to one window our tab bar is going to be
                // destroyed so unset it so that the later logic doesn't try to
                // use it.
                1 => {},

                // If our tab bar is visible and we are going down to 1 window,
                // hide the tab bar. The check is "2" because our current window
                // is still present.
                2 => if (tabgroup.getProperty(bool, "tabBarVisible")) {
                    nswindow.msgSend(void, objc.sel("toggleTabBar:"), .{nswindow.value});
                },

                else => {},
            }
        }

        // We can now safely destroy our windows. We have to do this BEFORE
        // setting up the new focused window below.
        self.window.destroy();
        if (self.cursor) |c| {
            c.destroy();
            self.cursor = null;
        }
    }

    /// Checks if the glfw window is in fullscreen.
    pub fn isFullscreen(self: *Surface) bool {
        return self.window.getMonitor() != null;
    }

    /// Close this surface.
    pub fn close(self: *Surface, processActive: bool) void {
        _ = processActive;
        self.setShouldClose();
        self.deinit();
        self.app.app.alloc.destroy(self);
    }

    /// Set the initial window size. This is called exactly once at
    /// surface initialization time. This may be called before "self"
    /// is fully initialized.
    fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void {
        const monitor = self.window.getMonitor() orelse glfw.Monitor.getPrimary() orelse {
            log.warn("window is not on a monitor, not setting initial size", .{});
            return;
        };

        const workarea = monitor.getWorkarea();
        self.window.setSize(.{
            .width = @min(width, workarea.width),
            .height = @min(height, workarea.height),
        });
    }

    /// Set the initial window position. This is called exactly once at
    /// surface initialization time. This may be called before "self"
    /// is fully initialized.
    fn setInitialWindowPosition(win: glfw.Window, x: ?i16, y: ?i16) void {
        const start_position_x = x orelse return;
        const start_position_y = y orelse return;

        log.debug("setting initial window position ({},{})", .{ start_position_x, start_position_y });
        win.setPos(.{ .x = start_position_x, .y = start_position_y });
    }

    /// Set the size limits of the window.
    /// Note: this interface is not good, we should redo it if we plan
    /// to use this more. i.e. you can't set max width but no max height,
    /// or no mins.
    fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
        self.window.setSizeLimits(.{
            .width = min.width,
            .height = min.height,
        }, if (max_) |max| .{
            .width = max.width,
            .height = max.height,
        } else .{
            .width = null,
            .height = null,
        });
    }

    /// Returns the content scale for the created window.
    pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
        const scale = self.window.getContentScale();
        return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale };
    }

    /// Returns the size of the window in pixels. The pixel size may
    /// not match screen coordinate size but we should be able to convert
    /// back and forth using getContentScale.
    pub fn getSize(self: *const Surface) !apprt.SurfaceSize {
        const size = self.window.getFramebufferSize();
        return apprt.SurfaceSize{ .width = size.width, .height = size.height };
    }

    /// Returns the cursor position in scaled pixels relative to the
    /// upper-left of the window.
    pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
        const unscaled_pos = self.window.getCursorPos();
        const pos = try self.cursorPosToPixels(unscaled_pos);
        return apprt.CursorPos{
            .x = @floatCast(pos.xpos),
            .y = @floatCast(pos.ypos),
        };
    }

    /// Set the flag that notes this window should be closed for the next
    /// iteration of the event loop.
    pub fn setShouldClose(self: *Surface) void {
        self.window.setShouldClose(true);
    }

    /// Returns true if the window is flagged to close.
    pub fn shouldClose(self: *const Surface) bool {
        return self.window.shouldClose();
    }

    /// Set the title of the window.
    fn setTitle(self: *Surface, slice: [:0]const u8) !void {
        if (self.title_text) |t| self.core_surface.alloc.free(t);
        self.title_text = try self.core_surface.alloc.dupeZ(u8, slice);
        self.window.setTitle(self.title_text.?.ptr);
    }

    /// Return the title of the window.
    pub fn getTitle(self: *Surface) ?[:0]const u8 {
        return self.title_text;
    }

    /// Set the shape of the cursor.
    fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
        if ((comptime builtin.target.isDarwin()) and
            !internal_os.macos.isAtLeastVersion(13, 0, 0))
        {
            // We only set our cursor if we're NOT on Mac, or if we are then the
            // macOS version is >= 13 (Ventura). On prior versions, glfw crashes
            // since we use a tab group.
            return;
        }

        const new = glfw.Cursor.createStandard(switch (shape) {
            .default => .arrow,
            .text => .ibeam,
            .crosshair => .crosshair,
            .pointer => .pointing_hand,
            .ew_resize => .resize_ew,
            .ns_resize => .resize_ns,
            .nwse_resize => .resize_nwse,
            .nesw_resize => .resize_nesw,
            .all_scroll => .resize_all,
            .not_allowed => .not_allowed,
            else => return, // unsupported, ignore
        }) orelse {
            const err = glfw.mustGetErrorCode();
            log.warn("error creating cursor: {}", .{err});
            return;
        };
        errdefer new.destroy();

        // Set our cursor before we destroy the old one
        self.window.setCursor(new);

        if (self.cursor) |c| c.destroy();
        self.cursor = new;
    }

    /// Set the visibility of the mouse cursor.
    fn setMouseVisibility(self: *Surface, visible: bool) void {
        self.window.setInputModeCursor(if (visible) .normal else .hidden);
    }

    pub fn supportsClipboard(
        self: *const Surface,
        clipboard_type: apprt.Clipboard,
    ) bool {
        _ = self;
        return switch (clipboard_type) {
            .standard => true,
            .selection, .primary => comptime builtin.os.tag == .linux,
        };
    }

    /// Start an async clipboard request.
    pub fn clipboardRequest(
        self: *Surface,
        clipboard_type: apprt.Clipboard,
        state: apprt.ClipboardRequest,
    ) !void {
        // GLFW can read clipboards immediately so just do that.
        const str: [:0]const u8 = switch (clipboard_type) {
            .standard => glfw.getClipboardString() orelse return glfw.mustGetErrorCode(),
            .selection, .primary => selection: {
                // Not supported except on Linux
                if (comptime builtin.os.tag != .linux) break :selection "";

                const raw = glfwNative.getX11SelectionString() orelse
                    return glfw.mustGetErrorCode();
                break :selection std.mem.span(raw);
            },
        };

        // Complete our request. We always allow unsafe because we don't
        // want to deal with user confirmation in this runtime.
        try self.core_surface.completeClipboardRequest(state, str, true);
    }

    /// Set the clipboard.
    pub fn setClipboardString(
        self: *const Surface,
        val: [:0]const u8,
        clipboard_type: apprt.Clipboard,
        confirm: bool,
    ) !void {
        _ = confirm;
        _ = self;
        switch (clipboard_type) {
            .standard => glfw.setClipboardString(val),
            .selection, .primary => {
                // Not supported except on Linux
                if (comptime builtin.os.tag != .linux) return;
                glfwNative.setX11SelectionString(val.ptr);
            },
        }
    }

    /// The cursor position from glfw directly is in screen coordinates but
    /// all our interface works in pixels.
    fn cursorPosToPixels(self: *const Surface, pos: glfw.Window.CursorPos) !glfw.Window.CursorPos {
        // The cursor position is in screen coordinates but we
        // want it in pixels. we need to get both the size of the
        // window in both to get the ratio to make the conversion.
        const size = self.window.getSize();
        const fb_size = self.window.getFramebufferSize();

        // If our framebuffer and screen are the same, then there is no scaling
        // happening and we can short-circuit by returning the pos as-is.
        if (fb_size.width == size.width and fb_size.height == size.height)
            return pos;

        const x_scale = @as(f64, @floatFromInt(fb_size.width)) / @as(f64, @floatFromInt(size.width));
        const y_scale = @as(f64, @floatFromInt(fb_size.height)) / @as(f64, @floatFromInt(size.height));
        return .{
            .xpos = pos.xpos * x_scale,
            .ypos = pos.ypos * y_scale,
        };
    }

    pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
        return try internal_os.getEnvMap(self.app.app.alloc);
    }

    fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
        _ = width;
        _ = height;

        // Get the size. We are given a width/height but this is in screen
        // coordinates and we want raw pixels. The core window uses the content
        // scale to scale appropriately.
        const core_win = window.getUserPointer(CoreSurface) orelse return;
        const size = core_win.rt_surface.getSize() catch |err| {
            log.err("error querying window size for size callback err={}", .{err});
            return;
        };

        // Call the primary callback.
        core_win.sizeCallback(size) catch |err| {
            log.err("error in size callback err={}", .{err});
            return;
        };
    }

    fn charCallback(window: glfw.Window, codepoint: u21) void {
        const core_win = window.getUserPointer(CoreSurface) orelse return;

        // We need a key event in order to process the charcallback. If it
        // isn't set then the key event was consumed.
        var key_event = core_win.rt_surface.key_event orelse return;
        core_win.rt_surface.key_event = null;

        // Populate the utf8 value for the event
        var buf: [4]u8 = undefined;
        const len = std.unicode.utf8Encode(codepoint, &buf) catch |err| {
            log.err("error encoding codepoint={} err={}", .{ codepoint, err });
            return;
        };
        key_event.utf8 = buf[0..len];

        // On macOS we need to also disable some modifiers because
        // alt+key consumes the alt.
        if (comptime builtin.target.isDarwin()) {
            // For GLFW, we say we always consume alt because
            // GLFW doesn't have a way to disable the alt key.
            key_event.consumed_mods.alt = true;
        }

        _ = core_win.keyCallback(key_event) catch |err| {
            log.err("error in key callback err={}", .{err});
            return;
        };
    }

    fn keyCallback(
        window: glfw.Window,
        glfw_key: glfw.Key,
        scancode: i32,
        glfw_action: glfw.Action,
        glfw_mods: glfw.Mods,
    ) void {
        _ = scancode;

        const core_win = window.getUserPointer(CoreSurface) orelse return;

        // Convert our glfw types into our input types
        const mods: input.Mods = .{
            .shift = glfw_mods.shift,
            .ctrl = glfw_mods.control,
            .alt = glfw_mods.alt,
            .super = glfw_mods.super,
        };
        const action: input.Action = switch (glfw_action) {
            .release => .release,
            .press => .press,
            .repeat => .repeat,
        };
        const key: input.Key = switch (glfw_key) {
            .a => .a,
            .b => .b,
            .c => .c,
            .d => .d,
            .e => .e,
            .f => .f,
            .g => .g,
            .h => .h,
            .i => .i,
            .j => .j,
            .k => .k,
            .l => .l,
            .m => .m,
            .n => .n,
            .o => .o,
            .p => .p,
            .q => .q,
            .r => .r,
            .s => .s,
            .t => .t,
            .u => .u,
            .v => .v,
            .w => .w,
            .x => .x,
            .y => .y,
            .z => .z,
            .zero => .zero,
            .one => .one,
            .two => .two,
            .three => .three,
            .four => .four,
            .five => .five,
            .six => .six,
            .seven => .seven,
            .eight => .eight,
            .nine => .nine,
            .up => .up,
            .down => .down,
            .right => .right,
            .left => .left,
            .home => .home,
            .end => .end,
            .page_up => .page_up,
            .page_down => .page_down,
            .escape => .escape,
            .F1 => .f1,
            .F2 => .f2,
            .F3 => .f3,
            .F4 => .f4,
            .F5 => .f5,
            .F6 => .f6,
            .F7 => .f7,
            .F8 => .f8,
            .F9 => .f9,
            .F10 => .f10,
            .F11 => .f11,
            .F12 => .f12,
            .F13 => .f13,
            .F14 => .f14,
            .F15 => .f15,
            .F16 => .f16,
            .F17 => .f17,
            .F18 => .f18,
            .F19 => .f19,
            .F20 => .f20,
            .F21 => .f21,
            .F22 => .f22,
            .F23 => .f23,
            .F24 => .f24,
            .F25 => .f25,
            .kp_0 => .kp_0,
            .kp_1 => .kp_1,
            .kp_2 => .kp_2,
            .kp_3 => .kp_3,
            .kp_4 => .kp_4,
            .kp_5 => .kp_5,
            .kp_6 => .kp_6,
            .kp_7 => .kp_7,
            .kp_8 => .kp_8,
            .kp_9 => .kp_9,
            .kp_decimal => .kp_decimal,
            .kp_divide => .kp_divide,
            .kp_multiply => .kp_multiply,
            .kp_subtract => .kp_subtract,
            .kp_add => .kp_add,
            .kp_enter => .kp_enter,
            .kp_equal => .kp_equal,
            .grave_accent => .grave_accent,
            .minus => .minus,
            .equal => .equal,
            .space => .space,
            .semicolon => .semicolon,
            .apostrophe => .apostrophe,
            .comma => .comma,
            .period => .period,
            .slash => .slash,
            .left_bracket => .left_bracket,
            .right_bracket => .right_bracket,
            .backslash => .backslash,
            .enter => .enter,
            .tab => .tab,
            .backspace => .backspace,
            .insert => .insert,
            .delete => .delete,
            .caps_lock => .caps_lock,
            .scroll_lock => .scroll_lock,
            .num_lock => .num_lock,
            .print_screen => .print_screen,
            .pause => .pause,
            .left_shift => .left_shift,
            .left_control => .left_control,
            .left_alt => .left_alt,
            .left_super => .left_super,
            .right_shift => .right_shift,
            .right_control => .right_control,
            .right_alt => .right_alt,
            .right_super => .right_super,

            .menu,
            .world_1,
            .world_2,
            .unknown,
            => .invalid,
        };

        // This is a hack for GLFW. We require our apprts to send both
        // the UTF8 encoding AND the keypress at the same time. Its critical
        // for things like ctrl sequences to work. However, GLFW doesn't
        // provide this information all at once. So we just infer based on
        // the key press. This isn't portable but GLFW is only for testing.
        const utf8 = switch (key) {
            inline else => |k| utf8: {
                if (mods.shift) break :utf8 "";
                const cp = k.codepoint() orelse break :utf8 "";
                const byte = std.math.cast(u8, cp) orelse break :utf8 "";
                break :utf8 &.{byte};
            },
        };

        const key_event: input.KeyEvent = .{
            .action = action,
            .key = key,
            .physical_key = key,
            .mods = mods,
            .consumed_mods = .{},
            .composing = false,
            .utf8 = utf8,
            .unshifted_codepoint = if (utf8.len > 0) @intCast(utf8[0]) else 0,
        };

        const effect = core_win.keyCallback(key_event) catch |err| {
            log.err("error in key callback err={}", .{err});
            return;
        };

        // Surface closed.
        if (effect == .closed) return;

        // If it wasn't consumed, we set it on our self so that charcallback
        // can make another attempt. Otherwise, we set null so the charcallback
        // is ignored.
        core_win.rt_surface.key_event = null;
        if (effect == .ignored and
            (action == .press or action == .repeat))
        {
            core_win.rt_surface.key_event = key_event;
        }
    }

    fn focusCallback(window: glfw.Window, focused: bool) void {
        const core_win = window.getUserPointer(CoreSurface) orelse return;
        core_win.focusCallback(focused) catch |err| {
            log.err("error in focus callback err={}", .{err});
            return;
        };
    }

    fn refreshCallback(window: glfw.Window) void {
        const core_win = window.getUserPointer(CoreSurface) orelse return;
        core_win.refreshCallback() catch |err| {
            log.err("error in refresh callback err={}", .{err});
            return;
        };
    }

    fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
        // Glfw doesn't support any of the scroll mods.
        const scroll_mods: input.ScrollMods = .{};

        const core_win = window.getUserPointer(CoreSurface) orelse return;
        core_win.scrollCallback(xoff, yoff, scroll_mods) catch |err| {
            log.err("error in scroll callback err={}", .{err});
            return;
        };
    }

    fn cursorPosCallback(
        window: glfw.Window,
        unscaled_xpos: f64,
        unscaled_ypos: f64,
    ) void {
        const core_win = window.getUserPointer(CoreSurface) orelse return;

        // Convert our unscaled x/y to scaled.
        const pos = core_win.rt_surface.cursorPosToPixels(.{
            .xpos = unscaled_xpos,
            .ypos = unscaled_ypos,
        }) catch |err| {
            log.err(
                "error converting cursor pos to scaled pixels in cursor pos callback err={}",
                .{err},
            );
            return;
        };

        core_win.cursorPosCallback(.{
            .x = @floatCast(pos.xpos),
            .y = @floatCast(pos.ypos),
        }, null) catch |err| {
            log.err("error in cursor pos callback err={}", .{err});
            return;
        };
    }

    fn mouseButtonCallback(
        window: glfw.Window,
        glfw_button: glfw.MouseButton,
        glfw_action: glfw.Action,
        glfw_mods: glfw.Mods,
    ) void {
        const core_win = window.getUserPointer(CoreSurface) orelse return;

        // Convert glfw button to input button
        const mods: input.Mods = .{
            .shift = glfw_mods.shift,
            .ctrl = glfw_mods.control,
            .alt = glfw_mods.alt,
            .super = glfw_mods.super,
        };
        const button: input.MouseButton = switch (glfw_button) {
            .left => .left,
            .right => .right,
            .middle => .middle,
            .four => .four,
            .five => .five,
            .six => .six,
            .seven => .seven,
            .eight => .eight,
        };
        const action: input.MouseButtonState = switch (glfw_action) {
            .press => .press,
            .release => .release,
            else => unreachable,
        };

        _ = core_win.mouseButtonCallback(action, button, mods) catch |err| {
            log.err("error in scroll callback err={}", .{err});
            return;
        };
    }

    fn dropCallback(window: glfw.Window, paths: [][*:0]const u8) void {
        const surface = window.getUserPointer(CoreSurface) orelse return;

        var list = std.ArrayList(u8).init(surface.alloc);
        defer list.deinit();

        for (paths) |path| {
            const path_slice = std.mem.span(path);

            // preallocate worst case of escaping every char + space
            list.ensureTotalCapacity(path_slice.len * 2 + 1) catch |err| {
                log.err("error in drop callback err={}", .{err});
                return;
            };

            const writer = list.writer();
            for (path_slice) |c| {
                if (std.mem.indexOfScalar(u8, "\\ ()[]{}<>\"'`!#$&;|*?\t", c)) |_| {
                    writer.print("\\{c}", .{c}) catch unreachable; //  memory preallocated
                } else writer.writeByte(c) catch unreachable; // same here
            }
            writer.writeByte(' ') catch unreachable; // separate paths

            surface.textCallback(list.items) catch |err| {
                log.err("error in drop callback err={}", .{err});
                return;
            };

            list.clearRetainingCapacity(); // avoid unnecessary reallocations
        }
    }
};
