//! X11 window protocol implementation for the Ghostty GTK apprt.
const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options");
const Allocator = std.mem.Allocator;
const c = @import("../c.zig").c;
const input = @import("../../../input.zig");
const Config = @import("../../../config.zig").Config;
const adwaita = @import("../adwaita.zig");

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

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

    pub fn init(
        alloc: Allocator,
        gdk_display: *c.GdkDisplay,
        app_id: [:0]const u8,
        config: *const Config,
    ) !?App {
        _ = alloc;

        // If the display isn't X11, then we don't need to do anything.
        if (c.g_type_check_instance_is_a(
            @ptrCast(@alignCast(gdk_display)),
            c.gdk_x11_display_get_type(),
        ) == 0) return null;

        // Get our X11 display
        const display: *c.Display = c.gdk_x11_display_get_xdisplay(
            gdk_display,
        ) orelse return error.NoX11Display;

        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 are 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.
        c.g_set_prgname(x11_program_name);
        c.gdk_x11_display_set_program_class(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(
            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(
            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 = display,
            .base_event_code = base_event_code,
            .atoms = Atoms.init(gdk_display),
        };
    }

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

    /// 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: ?*c.GdkDevice,
        gtk_mods: c.GdkModifierType,
    ) ?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(self.display, c.QueuedAfterReading) == 0) return null;

        var nextEvent: c.XEvent = undefined;
        _ = c.XPeekEvent(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 const Window = struct {
    app: *App,
    alloc: Allocator,
    config: DerivedConfig,
    window: c.Window,
    gtk_window: *c.GtkWindow,

    blur_region: Region = .{},

    const DerivedConfig = struct {
        blur: bool,
        window_decoration: Config.WindowDecoration,

        pub fn init(config: *const Config) DerivedConfig {
            return .{
                .blur = config.@"background-blur".enabled(),
                .window_decoration = config.@"window-decoration",
            };
        }
    };

    pub fn init(
        alloc: Allocator,
        app: *App,
        gtk_window: *c.GtkWindow,
        config: *const Config,
    ) !Window {
        const surface = c.gtk_native_get_surface(
            @ptrCast(gtk_window),
        ) orelse return error.NotX11Surface;

        // Check if we're actually on X11
        if (c.g_type_check_instance_is_a(
            @ptrCast(@alignCast(surface)),
            c.gdk_x11_surface_get_type(),
        ) == 0) return error.NotX11Surface;

        return .{
            .app = app,
            .alloc = alloc,
            .config = DerivedConfig.init(config),
            .window = c.gdk_x11_surface_get_xid(surface),
            .gtk_window = gtk_window,
        };
    }

    pub fn deinit(self: Window, alloc: Allocator) void {
        _ = self;
        _ = alloc;
    }

    pub fn updateConfigEvent(
        self: *Window,
        config: *const Config,
    ) !void {
        self.config = DerivedConfig.init(config);
    }

    pub fn resizeEvent(self: *Window) !void {
        // The blur region must update with window resizes
        self.blur_region.width = c.gtk_widget_get_width(@ptrCast(self.gtk_window));
        self.blur_region.height = c.gtk_widget_get_height(@ptrCast(self.gtk_window));
        try self.syncBlur();
    }

    pub fn syncAppearance(self: *Window) !void {
        self.blur_region = blur: {
            // NOTE(pluiedev): CSDs are a f--king mistake.
            // Please, GNOME, stop this nonsense of making a window ~30% bigger
            // internally than how they really are just for your shadows and
            // rounded corners and all that fluff. Please. I beg of you.
            var x: f64 = 0;
            var y: f64 = 0;
            c.gtk_native_get_surface_transform(
                @ptrCast(self.gtk_window),
                &x,
                &y,
            );

            break :blur .{
                .x = @intFromFloat(x),
                .y = @intFromFloat(y),
            };
        };
        self.syncBlur() catch |err| {
            log.err("failed to synchronize blur={}", .{err});
        };
        self.syncDecorations() catch |err| {
            log.err("failed to synchronize decorations={}", .{err});
        };
    }

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

    fn syncBlur(self: *Window) !void {
        // FIXME: This doesn't currently factor in rounded corners on Adwaita,
        // which means that the blur region will grow slightly outside of the
        // window borders. Unfortunately, actually calculating the rounded
        // region can be quite complex without having access to existing APIs
        // (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
        // and I think it's not really noticeable enough to justify the effort.
        // (Wayland also has this visual artifact anyway...)

        const blur = self.config.blur;
        log.debug("set blur={}, window xid={}, region={}", .{
            blur,
            self.window,
            self.blur_region,
        });

        if (blur) {
            try self.changeProperty(
                Region,
                self.app.atoms.kde_blur,
                c.XA_CARDINAL,
                ._32,
                .{ .mode = .replace },
                &self.blur_region,
            );
        } else {
            try self.deleteProperty(self.app.atoms.kde_blur);
        }
    }

    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.config.window_decoration) {
            .server => true,
            .auto, .client, .none => false,
        };

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

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

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

    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(
            self.app.display,
            self.window,
            name,
            options.offset,
            options.length,
            @intFromBool(options.delete),
            typ,
            &actual_type_return,
            &actual_format_return,
            &nitems_return,
            &bytes_after_return,
            &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,
        },
        value: *T,
    ) X11Error!void {
        const data: format.bufferType() = @ptrCast(value);

        const status = c.XChangeProperty(
            self.app.display,
            self.window,
            name,
            typ,
            @intFromEnum(format),
            @intFromEnum(options.mode),
            data,
            @divExact(@sizeOf(T), @sizeOf(format.elemType())),
        );

        // 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(self.app.display, self.window, 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: *c.GdkDisplay) Atoms {
        return .{
            .kde_blur = c.gdk_x11_get_xatom_by_name_for_display(
                display,
                "_KDE_NET_WM_BLUR_BEHIND_REGION",
            ),
            .motif_wm_hints = c.gdk_x11_get_xatom_by_name_for_display(
                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;
    }
};

const Region = extern struct {
    x: c_long = 0,
    y: c_long = 0,
    width: c_long = 0,
    height: c_long = 0,
};

// 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,
};
