/// A Window is a single, real GTK window that holds terminal surfaces.
///
/// A Window always contains a notebook (what GTK calls a tabbed container)
/// even while no tabs are in use, because a notebook without a tab bar has
/// no visible UI chrome.
const Window = @This();

const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;

const adw = @import("adw");
const gdk = @import("gdk");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");

const build_config = @import("../../build_config.zig");
const configpkg = @import("../../config.zig");
const font = @import("../../font/main.zig");
const i18n = @import("../../os/main.zig").i18n;
const input = @import("../../input.zig");
const CoreSurface = @import("../../Surface.zig");

const App = @import("App.zig");
const Builder = @import("Builder.zig");
const Color = configpkg.Config.Color;
const Surface = @import("Surface.zig");
const Menu = @import("menu.zig").Menu;
const Tab = @import("Tab.zig");
const gtk_key = @import("key.zig");
const TabView = @import("TabView.zig");
const HeaderBar = @import("headerbar.zig");
const CloseDialog = @import("CloseDialog.zig");
const CommandPalette = @import("CommandPalette.zig");
const winprotopkg = @import("winproto.zig");
const gtk_version = @import("gtk_version.zig");
const adw_version = @import("adw_version.zig");

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

app: *App,

/// Used to deduplicate updateConfig invocations
last_config: usize,

/// Local copy of any configuration
config: DerivedConfig,

/// Our window
window: *adw.ApplicationWindow,

/// The header bar for the window.
headerbar: HeaderBar,

/// The tab bar for the window.
tab_bar: *adw.TabBar,

/// The tab overview for the window. This is possibly null since there is no
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
tab_overview: ?*adw.TabOverview,

/// The notebook (tab grouping) for this window.
notebook: TabView,

/// The "main" menu that is attached to a button in the headerbar.
titlebar_menu: Menu(Window, "titlebar_menu", true),

/// The libadwaita widget for receiving toast send requests.
toast_overlay: *adw.ToastOverlay,

/// The command palette.
command_palette: CommandPalette,

/// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c_uint = null,

/// State and logic for windowing protocol for a window.
winproto: winprotopkg.Window,

pub const DerivedConfig = struct {
    background_opacity: f64,
    background_blur: configpkg.Config.BackgroundBlur,
    window_theme: configpkg.Config.WindowTheme,
    gtk_titlebar: bool,
    gtk_titlebar_hide_when_maximized: bool,
    gtk_tabs_location: configpkg.Config.GtkTabsLocation,
    gtk_wide_tabs: bool,
    gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
    window_show_tab_bar: configpkg.Config.WindowShowTabBar,

    quick_terminal_position: configpkg.Config.QuickTerminalPosition,
    quick_terminal_size: configpkg.Config.QuickTerminalSize,
    quick_terminal_autohide: bool,
    quick_terminal_keyboard_interactivity: configpkg.Config.QuickTerminalKeyboardInteractivity,

    maximize: bool,
    fullscreen: bool,
    window_decoration: configpkg.Config.WindowDecoration,

    pub fn init(config: *const configpkg.Config) DerivedConfig {
        return .{
            .background_opacity = config.@"background-opacity",
            .background_blur = config.@"background-blur",
            .window_theme = config.@"window-theme",
            .gtk_titlebar = config.@"gtk-titlebar",
            .gtk_titlebar_hide_when_maximized = config.@"gtk-titlebar-hide-when-maximized",
            .gtk_tabs_location = config.@"gtk-tabs-location",
            .gtk_wide_tabs = config.@"gtk-wide-tabs",
            .gtk_toolbar_style = config.@"gtk-toolbar-style",
            .window_show_tab_bar = config.@"window-show-tab-bar",

            .quick_terminal_position = config.@"quick-terminal-position",
            .quick_terminal_size = config.@"quick-terminal-size",
            .quick_terminal_autohide = config.@"quick-terminal-autohide",
            .quick_terminal_keyboard_interactivity = config.@"quick-terminal-keyboard-interactivity",

            .maximize = config.maximize,
            .fullscreen = config.fullscreen,
            .window_decoration = config.@"window-decoration",
        };
    }
};

pub fn create(alloc: Allocator, app: *App) !*Window {
    // Allocate a fixed pointer for our window. We try to minimize
    // allocations but windows and other GUI requirements are so minimal
    // compared to the steady-state terminal operation so we use heap
    // allocation for this.
    //
    // The allocation is owned by the GtkWindow created. It will be
    // freed when the window is closed.
    var window = try alloc.create(Window);
    errdefer alloc.destroy(window);
    try window.init(app);
    return window;
}

pub fn init(self: *Window, app: *App) !void {
    // Set up our own state
    self.* = .{
        .app = app,
        .last_config = @intFromPtr(&app.config),
        .config = .init(&app.config),
        .window = undefined,
        .headerbar = undefined,
        .tab_bar = undefined,
        .tab_overview = null,
        .notebook = undefined,
        .titlebar_menu = undefined,
        .toast_overlay = undefined,
        .command_palette = undefined,
        .winproto = .none,
    };

    // Create the window
    self.window = .new(app.app.as(gtk.Application));
    const gtk_window = self.window.as(gtk.Window);
    const gtk_widget = self.window.as(gtk.Widget);
    errdefer gtk_window.destroy();

    gtk_window.setTitle("Ghostty");
    gtk_window.setDefaultSize(1000, 600);
    gtk_widget.addCssClass("window");
    gtk_widget.addCssClass("terminal-window");

    // GTK4 grabs F10 input by default to focus the menubar icon. We want
    // to disable this so that terminal programs can capture F10 (such as htop)
    gtk_window.setHandleMenubarAccel(0);
    gtk_window.setIconName(build_config.bundle_id);

    // Create our box which will hold our widgets in the main content area.
    const box = gtk.Box.new(.vertical, 0);

    // Set up the menus
    self.titlebar_menu.init(self);

    // Setup our notebook
    self.notebook.init(self);

    if (adw_version.supportsDialogs()) try self.command_palette.init(self);

    // If we are using Adwaita, then we can support the tab overview.
    self.tab_overview = if (adw_version.supportsTabOverview()) overview: {
        const tab_overview = adw.TabOverview.new();
        tab_overview.setView(self.notebook.tab_view);
        tab_overview.setEnableNewTab(1);
        _ = adw.TabOverview.signals.create_tab.connect(
            tab_overview,
            *Window,
            gtkNewTabFromOverview,
            self,
            .{},
        );
        _ = gobject.Object.signals.notify.connect(
            tab_overview,
            *Window,
            adwTabOverviewOpen,
            self,
            .{
                .detail = "open",
            },
        );
        break :overview tab_overview;
    } else null;

    // gtk-titlebar can be used to disable the header bar (but keep the window
    // manager's decorations). We create this no matter if we are decorated or
    // not because we can have a keybind to toggle the decorations.
    self.headerbar.init(self);

    {
        const btn = gtk.MenuButton.new();
        btn.as(gtk.Widget).setTooltipText(i18n._("Main Menu"));
        btn.as(gtk.Widget).setCanFocus(0);
        btn.setIconName("open-menu-symbolic");
        btn.setPopover(self.titlebar_menu.asWidget());
        _ = gobject.Object.signals.notify.connect(
            btn,
            *Window,
            gtkTitlebarMenuActivate,
            self,
            .{
                .detail = "active",
            },
        );
        self.headerbar.packEnd(btn.as(gtk.Widget));
    }

    // If we're using an AdwWindow then we can support the tab overview.
    if (self.tab_overview) |tab_overview| {
        if (!adw_version.supportsTabOverview()) unreachable;

        const btn = switch (self.config.window_show_tab_bar) {
            .always, .auto => btn: {
                const btn = gtk.ToggleButton.new();
                btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs"));
                btn.as(gtk.Button).setIconName("view-grid-symbolic");
                _ = btn.as(gobject.Object).bindProperty(
                    "active",
                    tab_overview.as(gobject.Object),
                    "open",
                    .{ .bidirectional = true, .sync_create = true },
                );
                break :btn btn.as(gtk.Widget);
            },
            .never => btn: {
                const btn = adw.TabButton.new();
                btn.setView(self.notebook.tab_view);
                btn.as(gtk.Actionable).setActionName("overview.open");
                break :btn btn.as(gtk.Widget);
            },
        };

        btn.setCanFocus(0);
        btn.setFocusOnClick(0);
        self.headerbar.packEnd(btn);
    }

    {
        const btn = adw.SplitButton.new();
        btn.setIconName("tab-new-symbolic");
        btn.as(gtk.Widget).setTooltipText(i18n._("New Tab"));
        btn.setDropdownTooltip(i18n._("New Split"));

        var builder = Builder.init("menu-headerbar-split_menu", 1, 0);
        defer builder.deinit();
        btn.setMenuModel(builder.getObject(gio.MenuModel, "menu"));

        _ = adw.SplitButton.signals.clicked.connect(
            btn,
            *Window,
            adwNewTabClick,
            self,
            .{},
        );
        self.headerbar.packStart(btn.as(gtk.Widget));
    }

    _ = gobject.Object.signals.notify.connect(
        self.window,
        *Window,
        gtkWindowNotifyMaximized,
        self,
        .{
            .detail = "maximized",
        },
    );
    _ = gobject.Object.signals.notify.connect(
        self.window,
        *Window,
        gtkWindowNotifyFullscreened,
        self,
        .{
            .detail = "fullscreened",
        },
    );
    _ = gobject.Object.signals.notify.connect(
        self.window,
        *Window,
        gtkWindowNotifyIsActive,
        self,
        .{
            .detail = "is-active",
        },
    );
    _ = gobject.Object.signals.notify.connect(
        self.window,
        *Window,
        gtkWindowUpdateScaleFactor,
        self,
        .{
            .detail = "scale-factor",
        },
    );

    // If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
    // need to stick the headerbar into the content box.
    if (!adw_version.supportsTabOverview()) {
        box.append(self.headerbar.asWidget());
    }

    // In debug we show a warning and apply the 'devel' class to the window.
    // This is a really common issue where people build from source in debug and performance is really bad.
    if (comptime std.debug.runtime_safety) {
        const warning_box = gtk.Box.new(.vertical, 0);
        const warning_text = i18n._("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
        if (adw_version.supportsBanner()) {
            const banner = adw.Banner.new(warning_text);
            banner.setRevealed(1);
            warning_box.append(banner.as(gtk.Widget));
        } else {
            const warning = gtk.Label.new(warning_text);
            warning.as(gtk.Widget).setMarginTop(10);
            warning.as(gtk.Widget).setMarginBottom(10);
            warning_box.append(warning.as(gtk.Widget));
        }
        gtk_widget.addCssClass("devel");
        warning_box.as(gtk.Widget).addCssClass("background");
        box.append(warning_box.as(gtk.Widget));
    }

    // Setup our toast overlay if we have one
    self.toast_overlay = .new();
    self.toast_overlay.setChild(self.notebook.asWidget());
    box.append(self.toast_overlay.as(gtk.Widget));

    // If we have a tab overview then we can set it on our notebook.
    if (self.tab_overview) |tab_overview| {
        if (!adw_version.supportsTabOverview()) unreachable;
        tab_overview.setView(self.notebook.tab_view);
    }

    // We register a key event controller with the window so
    // we can catch key events when our surface may not be
    // focused (i.e. when the libadw tab overview is shown).
    const ec_key_press = gtk.EventControllerKey.new();
    errdefer ec_key_press.unref();
    gtk_widget.addController(ec_key_press.as(gtk.EventController));

    // All of our events
    _ = gtk.Widget.signals.realize.connect(
        self.window,
        *Window,
        gtkRealize,
        self,
        .{},
    );
    _ = gtk.Window.signals.close_request.connect(
        self.window,
        *Window,
        gtkCloseRequest,
        self,
        .{},
    );
    _ = gtk.Widget.signals.destroy.connect(
        self.window,
        *Window,
        gtkDestroy,
        self,
        .{},
    );
    _ = gtk.EventControllerKey.signals.key_pressed.connect(
        ec_key_press,
        *Window,
        gtkKeyPressed,
        self,
        .{},
    );

    // Our actions for the menu
    initActions(self);

    self.tab_bar = adw.TabBar.new();
    self.tab_bar.setView(self.notebook.tab_view);

    if (adw_version.supportsToolbarView()) {
        const toolbar_view = adw.ToolbarView.new();
        toolbar_view.addTopBar(self.headerbar.asWidget());

        switch (self.config.gtk_tabs_location) {
            .top => toolbar_view.addTopBar(self.tab_bar.as(gtk.Widget)),
            .bottom => toolbar_view.addBottomBar(self.tab_bar.as(gtk.Widget)),
        }
        toolbar_view.setContent(box.as(gtk.Widget));

        const toolbar_style: adw.ToolbarStyle = switch (self.config.gtk_toolbar_style) {
            .flat => .flat,
            .raised => .raised,
            .@"raised-border" => .raised_border,
        };
        toolbar_view.setTopBarStyle(toolbar_style);
        toolbar_view.setTopBarStyle(toolbar_style);

        // Set our application window content.
        self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget));
        self.window.setContent(self.tab_overview.?.as(gtk.Widget));
    } else {
        // In earlier adwaita versions, we need to add the tabbar manually since we do not use
        // an AdwToolbarView.
        self.tab_bar.as(gtk.Widget).addCssClass("inline");

        switch (self.config.gtk_tabs_location) {
            .top => box.insertChildAfter(
                self.tab_bar.as(gtk.Widget),
                self.headerbar.asWidget(),
            ),
            .bottom => box.append(self.tab_bar.as(gtk.Widget)),
        }
    }

    // If we want the window to be maximized, we do that here.
    if (self.config.maximize) self.window.as(gtk.Window).maximize();

    // If we are in fullscreen mode, new windows start fullscreen.
    if (self.config.fullscreen) self.window.as(gtk.Window).fullscreen();
}

pub fn present(self: *Window) void {
    self.window.as(gtk.Window).present();
}

pub fn toggleVisibility(self: *Window) void {
    const widget = self.window.as(gtk.Widget);

    widget.setVisible(@intFromBool(widget.isVisible() == 0));
}

pub fn isQuickTerminal(self: *Window) bool {
    return self.app.quick_terminal == self;
}

pub fn updateConfig(
    self: *Window,
    config: *const configpkg.Config,
) !void {
    // avoid multiple reconfigs when we have many surfaces contained in this
    // window using the integer value of config as a simple marker to know if
    // we've "seen" this particular config before
    const this_config = @intFromPtr(config);
    if (self.last_config == this_config) return;
    self.last_config = this_config;

    self.config = .init(config);

    // We always resync our appearance whenever the config changes.
    try self.syncAppearance();

    // Update binds inside the command palette
    try self.command_palette.updateConfig(config);
}

/// Updates appearance based on config settings. Will be called once upon window
/// realization, every time the config is reloaded, and every time a window state
/// is toggled (un-/maximized, un-/fullscreened, window decorations toggled, etc.)
///
/// TODO: Many of the initial style settings in `create` could possibly be made
/// reactive by moving them here.
pub fn syncAppearance(self: *Window) !void {
    const csd_enabled = self.winproto.clientSideDecorationEnabled();
    const gtk_window = self.window.as(gtk.Window);
    const gtk_widget = self.window.as(gtk.Widget);
    gtk_window.setDecorated(@intFromBool(csd_enabled));

    // Fix any artifacting that may occur in window corners. The .ssd CSS
    // class is defined in the GtkWindow documentation:
    // https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
    // for .ssd is provided by GTK and Adwaita.
    toggleCssClass(gtk_widget, "csd", csd_enabled);
    toggleCssClass(gtk_widget, "ssd", !csd_enabled);
    toggleCssClass(gtk_widget, "no-border-radius", !csd_enabled);

    self.headerbar.setVisible(visible: {
        // Never display the header bar when CSDs are disabled.
        if (!csd_enabled) break :visible false;

        // Never display the header bar as a quick terminal.
        if (self.isQuickTerminal()) break :visible false;

        // Unconditionally disable the header bar when fullscreened.
        if (self.window.as(gtk.Window).isFullscreen() != 0)
            break :visible false;

        // *Conditionally* disable the header bar when maximized,
        // and gtk-titlebar-hide-when-maximized is set
        if (self.window.as(gtk.Window).isMaximized() != 0 and
            self.config.gtk_titlebar_hide_when_maximized)
            break :visible false;

        break :visible self.config.gtk_titlebar;
    });

    toggleCssClass(
        gtk_widget,
        "background",
        self.config.background_opacity >= 1,
    );

    // Apply class to color headerbar if window-theme is set to `ghostty` and
    // GTK version is before 4.16. The conditional is because above 4.16
    // we use GTK CSS color variables.
    toggleCssClass(
        gtk_widget,
        "window-theme-ghostty",
        !gtk_version.atLeast(4, 16, 0) and self.config.window_theme == .ghostty,
    );

    if (self.tab_overview) |tab_overview| {
        if (!adw_version.supportsTabOverview()) unreachable;

        // Disable the title buttons (close, maximize, minimize, ...)
        // *inside* the tab overview if CSDs are disabled.
        // We do spare the search button, though.
        tab_overview.setShowStartTitleButtons(@intFromBool(csd_enabled));
        tab_overview.setShowEndTitleButtons(@intFromBool(csd_enabled));

        // Update toolbar view style
        toolbar_view: {
            const tab_overview_child = tab_overview.getChild() orelse break :toolbar_view;
            const toolbar_view = gobject.ext.cast(
                adw.ToolbarView,
                tab_overview_child,
            ) orelse break :toolbar_view;
            const toolbar_style: adw.ToolbarStyle = switch (self.config.gtk_toolbar_style) {
                .flat => .flat,
                .raised => .raised,
                .@"raised-border" => .raised_border,
            };
            toolbar_view.setTopBarStyle(toolbar_style);
            toolbar_view.setBottomBarStyle(toolbar_style);
        }
    }

    self.tab_bar.setExpandTabs(@intFromBool(self.config.gtk_wide_tabs));
    self.tab_bar.setAutohide(switch (self.config.window_show_tab_bar) {
        .auto, .never => @intFromBool(true),
        .always => @intFromBool(false),
    });
    self.tab_bar.as(gtk.Widget).setVisible(switch (self.config.window_show_tab_bar) {
        .always, .auto => @intFromBool(true),
        .never => @intFromBool(false),
    });

    self.winproto.syncAppearance() catch |err| {
        log.warn("failed to sync winproto appearance error={}", .{err});
    };
}

fn toggleCssClass(
    widget: *gtk.Widget,
    class: [:0]const u8,
    v: bool,
) void {
    if (v) {
        widget.addCssClass(class);
    } else {
        widget.removeCssClass(class);
    }
}

/// Sets up the GTK actions for the window scope. Actions are how GTK handles
/// menus and such. The menu is defined in App.zig but the action is defined
/// here. The string name binds them.
fn initActions(self: *Window) void {
    const window = self.window.as(gtk.ApplicationWindow);
    const action_map = window.as(gio.ActionMap);
    const actions = .{
        .{ "about", gtkActionAbout },
        .{ "close", gtkActionClose },
        .{ "new-window", gtkActionNewWindow },
        .{ "new-tab", gtkActionNewTab },
        .{ "close-tab", gtkActionCloseTab },
        .{ "split-right", gtkActionSplitRight },
        .{ "split-down", gtkActionSplitDown },
        .{ "split-left", gtkActionSplitLeft },
        .{ "split-up", gtkActionSplitUp },
        .{ "toggle-inspector", gtkActionToggleInspector },
        .{ "toggle-command-palette", gtkActionToggleCommandPalette },
        .{ "copy", gtkActionCopy },
        .{ "paste", gtkActionPaste },
        .{ "reset", gtkActionReset },
        .{ "clear", gtkActionClear },
        .{ "prompt-title", gtkActionPromptTitle },
    };

    inline for (actions) |entry| {
        const action = gio.SimpleAction.new(entry[0], null);
        defer action.unref();
        _ = gio.SimpleAction.signals.activate.connect(
            action,
            *Window,
            entry[1],
            self,
            .{},
        );
        action_map.addAction(action.as(gio.Action));
    }
}

pub fn deinit(self: *Window) void {
    self.winproto.deinit(self.app.core_app.alloc);
    if (adw_version.supportsDialogs()) self.command_palette.deinit();

    if (self.adw_tab_overview_focus_timer) |timer| {
        _ = glib.Source.remove(timer);
    }
}

/// Set the title of the window.
pub fn setTitle(self: *Window, title: [:0]const u8) void {
    self.headerbar.setTitle(title);
}

/// Set the subtitle of the window if it has one.
pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void {
    self.headerbar.setSubtitle(subtitle);
}

/// Add a new tab to this window.
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
    const alloc = self.app.core_app.alloc;
    _ = try Tab.create(alloc, self, parent);

    // TODO: When this is triggered through a GTK action, the new surface
    // redraws correctly. When it's triggered through keyboard shortcuts, it
    // does not (cursor doesn't blink) unless reactivated by refocusing.
}

/// Close the tab for the given notebook page. This will automatically
/// handle closing the window if there are no more tabs.
pub fn closeTab(self: *Window, tab: *Tab) void {
    self.notebook.closeTab(tab);
}

/// Go to the previous tab for a surface.
pub fn gotoPreviousTab(self: *Window, surface: *Surface) bool {
    const tab = surface.container.tab() orelse {
        log.info("surface is not attached to a tab bar, cannot navigate", .{});
        return false;
    };
    if (!self.notebook.gotoPreviousTab(tab)) return false;
    self.focusCurrentTab();
    return true;
}

/// Go to the next tab for a surface.
pub fn gotoNextTab(self: *Window, surface: *Surface) bool {
    const tab = surface.container.tab() orelse {
        log.info("surface is not attached to a tab bar, cannot navigate", .{});
        return false;
    };
    if (!self.notebook.gotoNextTab(tab)) return false;
    self.focusCurrentTab();
    return true;
}

/// Move the current tab for a surface.
pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
    const tab = surface.container.tab() orelse {
        log.info("surface is not attached to a tab bar, cannot navigate", .{});
        return;
    };
    self.notebook.moveTab(tab, position);
}

/// Go to the last tab for a surface.
pub fn gotoLastTab(self: *Window) bool {
    const max = self.notebook.nPages();
    return self.gotoTab(@intCast(max));
}

/// Go to the specific tab index.
pub fn gotoTab(self: *Window, n: usize) bool {
    if (n == 0) return false;
    const max = self.notebook.nPages();
    if (max == 0) return false;
    const page_idx = std.math.cast(c_int, n - 1) orelse return false;
    if (!self.notebook.gotoNthTab(@min(page_idx, max - 1))) return false;
    self.focusCurrentTab();
    return true;
}

/// Toggle tab overview (if present)
pub fn toggleTabOverview(self: *Window) void {
    if (self.tab_overview) |tab_overview| {
        if (!adw_version.supportsTabOverview()) unreachable;
        const is_open = tab_overview.getOpen() != 0;
        tab_overview.setOpen(@intFromBool(!is_open));
    }
}

/// Toggle the maximized state for this window.
pub fn toggleMaximize(self: *Window) void {
    if (self.window.as(gtk.Window).isMaximized() != 0) {
        self.window.as(gtk.Window).unmaximize();
    } else {
        self.window.as(gtk.Window).maximize();
    }
    // We update the config and call syncAppearance
    // in the gtkWindowNotifyMaximized callback
}

/// Toggle fullscreen for this window.
pub fn toggleFullscreen(self: *Window) void {
    if (self.window.as(gtk.Window).isFullscreen() != 0) {
        self.window.as(gtk.Window).unfullscreen();
    } else {
        self.window.as(gtk.Window).fullscreen();
    }
    // We update the config and call syncAppearance
    // in the gtkWindowNotifyFullscreened callback
}

/// Toggle the window decorations for this window.
pub fn toggleWindowDecorations(self: *Window) void {
    self.config.window_decoration = switch (self.config.window_decoration) {
        .none => switch (self.app.config.@"window-decoration") {
            // If we started as none, then we switch to auto
            .none => .auto,
            // Switch back
            .auto, .client, .server => |v| v,
        },
        // Always set to none
        .auto, .client, .server => .none,
    };

    self.syncAppearance() catch |err| {
        log.err("failed to sync appearance={}", .{err});
    };
}

/// Toggle the window decorations for this window.
pub fn toggleCommandPalette(self: *Window) void {
    if (adw_version.supportsDialogs()) {
        self.command_palette.toggle();
    } else {
        log.warn("libadwaita 1.5+ is required for the command palette", .{});
    }
}

/// Grabs focus on the currently selected tab.
pub fn focusCurrentTab(self: *Window) void {
    const tab = self.notebook.currentTab() orelse return;
    const surface = tab.focus_child orelse return;
    _ = surface.gl_area.as(gtk.Widget).grabFocus();

    if (surface.getTitle()) |title| {
        self.setTitle(title);
    }
}

pub fn onConfigReloaded(self: *Window) void {
    self.sendToast(i18n._("Reloaded the configuration"));
}

pub fn sendToast(self: *Window, title: [*:0]const u8) void {
    const toast = adw.Toast.new(title);
    toast.setTimeout(3);
    self.toast_overlay.addToast(toast);
}

fn gtkRealize(_: *adw.ApplicationWindow, self: *Window) callconv(.c) void {
    // Initialize our window protocol logic
    if (winprotopkg.Window.init(
        self.app.core_app.alloc,
        &self.app.winproto,
        self,
    )) |wp| {
        self.winproto = wp;
    } else |err| {
        log.warn("failed to initialize window protocol error={}", .{err});
    }

    // When we are realized we always setup our appearance
    self.syncAppearance() catch |err| {
        log.err("failed to initialize appearance={}", .{err});
    };
}

fn gtkWindowNotifyMaximized(
    _: *adw.ApplicationWindow,
    _: *gobject.ParamSpec,
    self: *Window,
) callconv(.c) void {
    self.syncAppearance() catch |err| {
        log.err("failed to sync appearance={}", .{err});
    };
}

fn gtkWindowNotifyFullscreened(
    _: *adw.ApplicationWindow,
    _: *gobject.ParamSpec,
    self: *Window,
) callconv(.c) void {
    self.syncAppearance() catch |err| {
        log.err("failed to sync appearance={}", .{err});
    };
}

fn gtkWindowNotifyIsActive(
    _: *adw.ApplicationWindow,
    _: *gobject.ParamSpec,
    self: *Window,
) callconv(.c) void {
    self.winproto.setUrgent(false) catch |err| {
        log.err("failed to unrequest user attention={}", .{err});
    };

    if (self.isQuickTerminal()) {
        // Hide when we're unfocused
        if (self.config.quick_terminal_autohide and self.window.as(gtk.Window).isActive() == 0) {
            self.toggleVisibility();
        }
    }
}

fn gtkWindowUpdateScaleFactor(
    _: *adw.ApplicationWindow,
    _: *gobject.ParamSpec,
    self: *Window,
) callconv(.c) void {
    // On some platforms (namely X11) we need to refresh our appearance when
    // the scale factor changes. In theory this could be more fine-grained as
    // a full refresh could be expensive, but a) this *should* be rare, and
    // b) quite noticeable visual bugs would occur if this is not present.
    self.winproto.syncAppearance() catch |err| {
        log.err(
            "failed to sync appearance after scale factor has been updated={}",
            .{err},
        );
        return;
    };
}

/// Perform a binding action on the window's action surface.
pub fn performBindingAction(self: *Window, action: input.Binding.Action) void {
    const surface = self.actionSurface() orelse return;
    _ = surface.performBindingAction(action) catch |err| {
        log.warn("error performing binding action error={}", .{err});
        return;
    };
}

fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void {
    self.performBindingAction(.{ .new_tab = {} });
}

/// Create a new surface (tab or split).
fn adwNewTabClick(_: *adw.SplitButton, self: *Window) callconv(.c) void {
    self.performBindingAction(.{ .new_tab = {} });
}

/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
/// because we need to return an AdwTabPage from this function.
fn gtkNewTabFromOverview(_: *adw.TabOverview, self: *Window) callconv(.c) *adw.TabPage {
    if (!adw_version.supportsTabOverview()) unreachable;

    const alloc = self.app.core_app.alloc;
    const surface = self.actionSurface();
    const tab = Tab.create(alloc, self, surface) catch unreachable;
    return self.notebook.tab_view.getPage(tab.box.as(gtk.Widget));
}

fn adwTabOverviewOpen(
    tab_overview: *adw.TabOverview,
    _: *gobject.ParamSpec,
    self: *Window,
) callconv(.c) void {
    if (!adw_version.supportsTabOverview()) unreachable;

    // We only care about when the tab overview is closed.
    if (tab_overview.getOpen() != 0) return;

    // On tab overview close, focus is sometimes lost. This is an
    // upstream issue in libadwaita[1]. When this is resolved we
    // can put a runtime version check here to avoid this workaround.
    //
    // Our workaround is to start a timer after 500ms to refocus
    // the currently selected tab. We choose 500ms because the adw
    // animation is 400ms.
    //
    // [1]: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/670

    // If we have an old timer remove it
    if (self.adw_tab_overview_focus_timer) |timer| {
        _ = glib.Source.remove(timer);
    }

    // Restart our timer
    self.adw_tab_overview_focus_timer = glib.timeoutAdd(
        500,
        adwTabOverviewFocusTimer,
        self,
    );
}

fn adwTabOverviewFocusTimer(
    ud: ?*anyopaque,
) callconv(.c) c_int {
    if (!adw_version.supportsTabOverview()) unreachable;
    const self: *Window = @ptrCast(@alignCast(ud orelse return 0));
    self.adw_tab_overview_focus_timer = null;
    self.focusCurrentTab();

    // Remove the timer
    return 0;
}

pub fn close(self: *Window) void {
    const window = self.window.as(gtk.Window);

    // Unset the quick terminal on the app level
    if (self.isQuickTerminal()) self.app.quick_terminal = null;

    window.destroy();
}

pub fn closeWithConfirmation(self: *Window) void {
    // If none of our surfaces need confirmation, we can just exit.
    for (self.app.core_app.surfaces.items) |surface| {
        if (surface.container.window()) |window| {
            if (window == self and
                surface.core_surface.needsConfirmQuit()) break;
        }
    } else {
        self.close();
        return;
    }

    CloseDialog.show(.{ .window = self }) catch |err| {
        log.err("failed to open close dialog={}", .{err});
    };
}

fn gtkCloseRequest(_: *adw.ApplicationWindow, self: *Window) callconv(.c) c_int {
    log.debug("window close request", .{});

    self.closeWithConfirmation();
    return 1;
}

/// "destroy" signal for the window
fn gtkDestroy(_: *adw.ApplicationWindow, self: *Window) callconv(.c) void {
    log.debug("window destroy", .{});

    const alloc = self.app.core_app.alloc;
    self.deinit();
    alloc.destroy(self);
}

fn gtkKeyPressed(
    ec_key: *gtk.EventControllerKey,
    keyval: c_uint,
    keycode: c_uint,
    gtk_mods: gdk.ModifierType,
    self: *Window,
) callconv(.c) c_int {
    // We only process window-level events currently for the tab
    // overview. This is primarily defensive programming because
    // I'm not 100% certain how our logic below will interact with
    // other parts of the application but I know for sure we must
    // handle this during the tab overview.
    //
    // If someone can confidently show or explain that this is not
    // necessary, please remove this check.
    if (adw_version.supportsTabOverview()) {
        if (self.tab_overview) |tab_overview| {
            if (tab_overview.getOpen() == 0) return 0;
        }
    }

    const surface = self.app.core_app.focusedSurface() orelse return 0;
    return if (surface.rt_surface.keyEvent(
        .press,
        ec_key,
        keyval,
        keycode,
        gtk_mods,
    )) 1 else 0;
}

fn gtkActionAbout(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    const name = "Ghostty";
    const icon = "com.mitchellh.ghostty";
    const website = "https://ghostty.org";

    if (adw_version.supportsDialogs()) {
        adw.showAboutDialog(
            self.window.as(gtk.Widget),
            "application-name",
            name,
            "developer-name",
            i18n._("Ghostty Developers"),
            "application-icon",
            icon,
            "version",
            build_config.version_string.ptr,
            "issue-url",
            "https://github.com/ghostty-org/ghostty/issues",
            "website",
            website,
            @as(?*anyopaque, null),
        );
    } else {
        gtk.showAboutDialog(
            self.window.as(gtk.Window),
            "program-name",
            name,
            "logo-icon-name",
            icon,
            "title",
            i18n._("About Ghostty"),
            "version",
            build_config.version_string.ptr,
            "website",
            website,
            @as(?*anyopaque, null),
        );
    }
}

fn gtkActionClose(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.closeWithConfirmation();
}

fn gtkActionNewWindow(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .new_window = {} });
}

fn gtkActionNewTab(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .new_tab = {} });
}

fn gtkActionCloseTab(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .close_tab = {} });
}

fn gtkActionSplitRight(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .new_split = .right });
}

fn gtkActionSplitDown(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .new_split = .down });
}

fn gtkActionSplitLeft(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .new_split = .left });
}

fn gtkActionSplitUp(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .new_split = .up });
}

fn gtkActionToggleInspector(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .inspector = .toggle });
}

fn gtkActionToggleCommandPalette(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.C) void {
    self.performBindingAction(.toggle_command_palette);
}

fn gtkActionCopy(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .copy_to_clipboard = {} });
}

fn gtkActionPaste(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .paste_from_clipboard = {} });
}

fn gtkActionReset(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .reset = {} });
}

fn gtkActionClear(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .clear_screen = {} });
}

fn gtkActionPromptTitle(
    _: *gio.SimpleAction,
    _: ?*glib.Variant,
    self: *Window,
) callconv(.c) void {
    self.performBindingAction(.{ .prompt_surface_title = {} });
}

/// Returns the surface to use for an action.
pub fn actionSurface(self: *Window) ?*CoreSurface {
    const tab = self.notebook.currentTab() orelse return null;
    const surface = tab.focus_child orelse return null;
    return &surface.core_surface;
}

fn gtkTitlebarMenuActivate(
    btn: *gtk.MenuButton,
    _: *gobject.ParamSpec,
    self: *Window,
) callconv(.c) void {
    // debian 12 is stuck on GTK 4.8
    if (!gtk_version.atLeast(4, 10, 0)) return;
    const active = btn.getActive() != 0;
    if (active) {
        self.titlebar_menu.refresh();
    } else {
        self.focusCurrentTab();
    }
}
