//! X11 window protocol implementation for the Ghostty GTK apprt.
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;

const gdk = @import("gdk");
const gdk_x11 = @import("gdk_x11");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const xlib = @import("xlib");

pub const c = @cImport({
    @cInclude("X11/Xlib.h");
    @cInclude("X11/Xatom.h");
    @cInclude("X11/XKBlib.h");
});

const input = @import("../../../input.zig");
const Config = @import("../../../config.zig").Config;
const ApprtWindow = @import("../class/window.zig").Window;
const GlobalShortcuts = @import("../class/global_shortcuts.zig").GlobalShortcuts;
const BlurRegion = @import("BlurRegion.zig");

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

pub const App = struct {
    display: *xlib.Display,
    base_event_code: c_int,
    atoms: Atoms,

    pub fn init(
        _: Allocator,
        gdk_display: *gdk.Display,
        app_id: [:0]const u8,
        config: *const Config,
    ) !?App {
        // If the display isn't X11, then we don't need to do anything.
        const gdk_x11_display = gobject.ext.cast(
            gdk_x11.X11Display,
            gdk_display,
        ) orelse return null;

        const xlib_display = gdk_x11_display.getXdisplay();

        const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
            pn
        else if (builtin.mode == .Debug)
            "ghostty-debug"
        else
            "ghostty";

        // Set the X11 window class property (WM_CLASS) if we are on an X11
        // display.
        //
        // Note that we also set the program name here using g_set_prgname.
        // This is how the instance name field for WM_CLASS is derived when
        // calling gdk_x11_display_set_program_class; there does not seem to be
        // a way to set it directly. It does not look like this is being set by
        // our other app initialization routines currently, but since we're
        // currently deriving its value from x11-instance-name effectively, I
        // feel like gating it behind an X11 check is better intent.
        //
        // This makes the property show up like so when using xprop:
        //
        //     WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
        //
        // Append "-debug" on both when using the debug build.
        glib.setPrgname(x11_program_name);
        gdk_x11.X11Display.setProgramClass(gdk_display, app_id);

        // XKB
        log.debug("Xkb.init: initializing Xkb", .{});
        log.debug("Xkb.init: running XkbQueryExtension", .{});
        var opcode: c_int = 0;
        var base_event_code: c_int = 0;
        var base_error_code: c_int = 0;
        var major = c.XkbMajorVersion;
        var minor = c.XkbMinorVersion;
        if (c.XkbQueryExtension(
            @ptrCast(@alignCast(xlib_display)),
            &opcode,
            &base_event_code,
            &base_error_code,
            &major,
            &minor,
        ) == 0) {
            log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
            return error.XkbInitializationError;
        }

        log.debug("Xkb.init: running XkbSelectEventDetails", .{});
        if (c.XkbSelectEventDetails(
            @ptrCast(@alignCast(xlib_display)),
            c.XkbUseCoreKbd,
            c.XkbStateNotify,
            c.XkbModifierStateMask,
            c.XkbModifierStateMask,
        ) == 0) {
            log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
            return error.XkbInitializationError;
        }

        return .{
            .display = xlib_display,
            .base_event_code = base_event_code,
            .atoms = .init(gdk_x11_display),
        };
    }

    pub fn deinit(self: *App) void {
        _ = self;
    }

    /// Checks for an immediate pending XKB state update event, and returns the
    /// keyboard state based on if it finds any. This is necessary as the
    /// standard GTK X11 API (and X11 in general) does not include the current
    /// key pressed in any modifier state snapshot for that event (e.g. if the
    /// pressed key is a modifier, that is not necessarily reflected in the
    /// modifiers).
    ///
    /// Returns null if there is no event. In this case, the caller should fall
    /// back to the standard GDK modifier state (this likely means the key
    /// event did not result in a modifier change).
    pub fn eventMods(
        self: App,
        device: ?*gdk.Device,
        gtk_mods: gdk.ModifierType,
    ) ?input.Mods {
        _ = device;
        _ = gtk_mods;

        // Shoutout to Mozilla for figuring out a clean way to do this, this is
        // paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
        if (c.XEventsQueued(
            @ptrCast(@alignCast(self.display)),
            c.QueuedAfterReading,
        ) == 0) return null;

        var nextEvent: c.XEvent = undefined;
        _ = c.XPeekEvent(@ptrCast(@alignCast(self.display)), &nextEvent);
        if (nextEvent.type != self.base_event_code) return null;

        const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
        if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;

        const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
        // Check the state according to XKB masks.
        const lookup_mods = xkb_state_notify_event.lookup_mods;
        var mods: input.Mods = .{};

        log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
        if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
        if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
        if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
        if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
        if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;

        return mods;
    }

    pub fn bindGlobalShortcuts(
        _: *App,
        _: *GlobalShortcuts,
        _: *const Config,
    ) bool {
        return false;
    }

    pub fn clearGlobalShortcuts(_: *App) void {}

    pub fn supportsQuickTerminal(_: App) bool {
        log.warn("quick terminal is not yet supported on X11", .{});
        return false;
    }

    pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
};

pub const Window = struct {
    app: *App,
    apprt_window: *ApprtWindow,
    x11_surface: *gdk_x11.X11Surface,
    alloc: Allocator,

    blur_region: BlurRegion = .empty,

    // Cache last applied values to avoid redundant X11 property updates.
    // Redundant property updates seem to cause some visual glitches
    // with some window managers: https://github.com/ghostty-org/ghostty/pull/8075
    last_applied_decoration_hints: ?MotifWMHints = null,

    pub fn init(
        alloc: Allocator,
        app: *App,
        apprt_window: *ApprtWindow,
    ) !Window {
        const surface = apprt_window.as(gtk.Native).getSurface() orelse
            return error.NotX11Surface;

        const x11_surface = gobject.ext.cast(
            gdk_x11.X11Surface,
            surface,
        ) orelse return error.NotX11Surface;

        return .{
            .app = app,
            .alloc = alloc,
            .apprt_window = apprt_window,
            .x11_surface = x11_surface,
        };
    }

    pub fn deinit(self: *Window) void {
        self.blur_region.deinit(self.alloc);
    }

    pub fn resizeEvent(self: *Window) !void {
        // The blur region must update with window resizes
        self.syncBlur() catch |err| {
            log.err("failed to sync blur={}", .{err});
        };
    }

    pub fn syncAppearance(self: *Window) !void {
        // The user could have toggled between CSDs and SSDs,
        // therefore we need to recalculate the blur region offset.
        self.syncBlur() catch |err| {
            log.err("failed to sync blur={}", .{err});
        };
        self.syncDecorations() catch |err| {
            log.err("failed to sync decorations={}", .{err});
        };
    }

    pub fn clientSideDecorationEnabled(self: Window) bool {
        return switch (self.apprt_window.getWindowDecoration()) {
            .auto, .client => true,
            .server, .none => false,
        };
    }

    fn syncBlur(self: *Window) !void {
        const config = if (self.apprt_window.getConfig()) |v| v.get() else return;

        // When blur is disabled, remove the property if it was previously set
        const blur = config.@"background-blur";

        var region: BlurRegion = if (blur.enabled())
            try .calcForWindow(
                self.alloc,
                self.apprt_window,
                self.clientSideDecorationEnabled(),
                true,
            )
        else
            .empty;
        errdefer region.deinit(self.alloc);

        // Only update X11 properties when the blur region actually changes
        if (region.eql(self.blur_region)) {
            region.deinit(self.alloc);
            return;
        }

        if (region.slices.items.len > 0) {
            log.debug("set blur={}, window xid={}, region={}", .{
                blur,
                self.x11_surface.getXid(),
                region,
            });

            try self.changeProperty(
                BlurRegion.Slice,
                self.app.atoms.kde_blur,
                c.XA_CARDINAL,
                ._32,
                .{ .mode = .replace },
                region.slices.items,
            );
        } else {
            try self.deleteProperty(self.app.atoms.kde_blur);
        }

        self.blur_region.deinit(self.alloc);
        self.blur_region = region;
    }

    fn syncDecorations(self: *Window) !void {
        var hints: MotifWMHints = .{};

        self.getWindowProperty(
            MotifWMHints,
            self.app.atoms.motif_wm_hints,
            self.app.atoms.motif_wm_hints,
            ._32,
            .{},
            &hints,
        ) catch |err| switch (err) {
            // motif_wm_hints is already initialized, so this is fine
            error.PropertyNotFound => {},

            error.RequestFailed,
            error.PropertyTypeMismatch,
            error.PropertyFormatMismatch,
            => return err,
        };

        hints.flags.decorations = true;
        hints.decorations.all = switch (self.apprt_window.getWindowDecoration()) {
            .server => true,
            .auto, .client, .none => false,
        };

        // Only update decoration hints when they actually change
        if (self.last_applied_decoration_hints) |last| {
            if (std.meta.eql(hints, last)) return;
        }

        try self.changeProperty(
            MotifWMHints,
            self.app.atoms.motif_wm_hints,
            self.app.atoms.motif_wm_hints,
            ._32,
            .{ .mode = .replace },
            &.{hints},
        );
        self.last_applied_decoration_hints = hints;
    }

    pub fn addSubprocessEnv(self: *Window, env: *std.process.Environ.Map) !void {
        var buf: [64]u8 = undefined;
        const window_id = try std.fmt.bufPrint(
            &buf,
            "{}",
            .{self.x11_surface.getXid()},
        );

        try env.put("WINDOWID", window_id);
    }

    pub fn setUrgent(self: *Window, urgent: bool) !void {
        self.x11_surface.setUrgencyHint(@intFromBool(urgent));
    }

    fn getWindowProperty(
        self: *Window,
        comptime T: type,
        name: c.Atom,
        typ: c.Atom,
        comptime format: PropertyFormat,
        options: struct {
            offset: c_long = 0,
            length: c_long = std.math.maxInt(c_long),
            delete: bool = false,
        },
        result: *T,
    ) GetWindowPropertyError!void {
        // FIXME: Maybe we should switch to libxcb one day.
        // Sounds like a much better idea than whatever this is
        var actual_type_return: c.Atom = undefined;
        var actual_format_return: c_int = undefined;
        var nitems_return: c_ulong = undefined;
        var bytes_after_return: c_ulong = undefined;
        var prop_return: ?format.bufferType() = null;

        const code = c.XGetWindowProperty(
            @ptrCast(@alignCast(self.app.display)),
            self.x11_surface.getXid(),
            name,
            options.offset,
            options.length,
            @intFromBool(options.delete),
            typ,
            &actual_type_return,
            &actual_format_return,
            &nitems_return,
            &bytes_after_return,
            @ptrCast(&prop_return),
        );
        if (code != c.Success) return error.RequestFailed;

        if (actual_type_return == c.None) return error.PropertyNotFound;
        if (typ != actual_type_return) return error.PropertyTypeMismatch;
        if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;

        const data_ptr: *T = @ptrCast(prop_return);
        result.* = data_ptr.*;
        _ = c.XFree(prop_return);
    }

    fn changeProperty(
        self: *Window,
        comptime T: type,
        name: c.Atom,
        typ: c.Atom,
        comptime format: PropertyFormat,
        options: struct {
            mode: PropertyChangeMode,
        },
        values: []const T,
    ) X11Error!void {
        const data: format.bufferType() = @ptrCast(@constCast(values));
        // The number of "words" that each element `T` occupies.
        const words_per_elem = @divExact(@sizeOf(T), @sizeOf(format.elemType()));

        const status = c.XChangeProperty(
            @ptrCast(@alignCast(self.app.display)),
            self.x11_surface.getXid(),
            name,
            typ,
            @intFromEnum(format),
            @intFromEnum(options.mode),
            data,
            @intCast(words_per_elem * values.len),
        );

        // For some godforsaken reason Xlib alternates between
        // error values (0 = success) and booleans (1 = success), and they look exactly
        // the same in the signature (just `int`, since Xlib is written in C89)...
        if (status == 0) return error.RequestFailed;
    }

    fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
        const status = c.XDeleteProperty(
            @ptrCast(@alignCast(self.app.display)),
            self.x11_surface.getXid(),
            name,
        );
        if (status == 0) return error.RequestFailed;
    }
};

const X11Error = error{
    RequestFailed,
};

const GetWindowPropertyError = X11Error || error{
    PropertyNotFound,
    PropertyTypeMismatch,
    PropertyFormatMismatch,
};

const Atoms = struct {
    kde_blur: c.Atom,
    motif_wm_hints: c.Atom,

    fn init(display: *gdk_x11.X11Display) Atoms {
        return .{
            .kde_blur = gdk_x11.x11GetXatomByNameForDisplay(
                display,
                "_KDE_NET_WM_BLUR_BEHIND_REGION",
            ),
            .motif_wm_hints = gdk_x11.x11GetXatomByNameForDisplay(
                display,
                "_MOTIF_WM_HINTS",
            ),
        };
    }
};

const PropertyChangeMode = enum(c_int) {
    replace = c.PropModeReplace,
    prepend = c.PropModePrepend,
    append = c.PropModeAppend,
};

const PropertyFormat = enum(c_int) {
    _8 = 8,
    _16 = 16,
    _32 = 32,

    fn elemType(comptime self: PropertyFormat) type {
        return switch (self) {
            ._8 => c_char,
            ._16 => c_int,
            ._32 => c_long,
        };
    }

    fn bufferType(comptime self: PropertyFormat) type {
        // The buffer type has to be a multi-pointer to bytes
        // *aligned to the element type* (very important,
        // otherwise you'll read garbage!)
        //
        // I know this is really ugly. X11 is ugly. I consider it apropos.
        return [*]align(@alignOf(self.elemType())) u8;
    }
};

// See Xm/MwmUtil.h, packaged with the Motif Window Manager
const MotifWMHints = extern struct {
    flags: packed struct(c_ulong) {
        _pad: u1 = 0,
        decorations: bool = false,

        // We don't really care about the other flags
        _rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
    } = .{},
    functions: c_ulong = 0,
    decorations: packed struct(c_ulong) {
        all: bool = false,

        // We don't really care about the other flags
        _rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
    } = .{},
    input_mode: c_long = 0,
    status: c_ulong = 0,
};
