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

const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;

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

/// The overlay that shows the current size while a surface is resizing.
/// This can be used generically to show pretty much anything with a
/// disappearing overlay, but we have no other use at this point so it
/// is named specifically for what it does.
///
/// General usage:
///
///   1. Add it to an overlay
///   2. Set the label with `setLabel`
///   3. Schedule to show it with `schedule`
///
/// Set any properties to change the behavior.
pub const ResizeOverlay = extern struct {
    const Self = @This();
    parent_instance: Parent,
    pub const Parent = adw.Bin;
    pub const getGObjectType = gobject.ext.defineClass(Self, .{
        .name = "GhosttyResizeOverlay",
        .instanceInit = &init,
        .classInit = &Class.init,
        .parent_class = &Class.parent,
        .private = .{ .Type = Private, .offset = &Private.offset },
    });

    pub const properties = struct {
        pub const duration = struct {
            pub const name = "duration";
            const impl = gobject.ext.defineProperty(
                name,
                Self,
                c_uint,
                .{
                    .default = 750,
                    .minimum = 250,
                    .maximum = std.math.maxInt(c_uint),
                    .accessor = gobject.ext.privateFieldAccessor(
                        Self,
                        Private,
                        &Private.offset,
                        "duration",
                    ),
                },
            );
        };

        pub const @"first-delay" = struct {
            pub const name = "first-delay";
            const impl = gobject.ext.defineProperty(
                name,
                Self,
                c_uint,
                .{
                    .default = 250,
                    .minimum = 250,
                    .maximum = std.math.maxInt(c_uint),
                    .accessor = gobject.ext.privateFieldAccessor(
                        Self,
                        Private,
                        &Private.offset,
                        "first_delay",
                    ),
                },
            );
        };

        pub const label = struct {
            pub const name = "label";
            const impl = gobject.ext.defineProperty(
                name,
                Self,
                ?[:0]const u8,
                .{
                    .default = null,
                    .accessor = C.privateStringFieldAccessor("label_text"),
                },
            );
        };

        pub const @"overlay-halign" = struct {
            pub const name = "overlay-halign";
            const impl = gobject.ext.defineProperty(
                name,
                Self,
                gtk.Align,
                .{
                    .default = .center,
                    .accessor = gobject.ext.privateFieldAccessor(
                        Self,
                        Private,
                        &Private.offset,
                        "halign",
                    ),
                },
            );
        };

        pub const @"overlay-valign" = struct {
            pub const name = "overlay-valign";
            const impl = gobject.ext.defineProperty(
                name,
                Self,
                gtk.Align,
                .{
                    .default = .center,
                    .accessor = gobject.ext.privateFieldAccessor(
                        Self,
                        Private,
                        &Private.offset,
                        "valign",
                    ),
                },
            );
        };
    };

    const Private = struct {
        /// The label with the text
        label: *gtk.Label,

        /// The text to set on the label when scheduled.
        label_text: ?[:0]const u8,

        /// The time that the overlay appears.
        duration: c_uint,

        /// The first delay before any overlay is shown. Must be specified
        /// during construction otherwise it has no effect.
        first_delay: c_uint,

        /// The idle source that we use to update the label.
        idler: ?c_uint = null,

        /// The timer for dismissing the overlay.
        timer: ?c_uint = null,

        /// The first delay timer.
        delay_timer: ?c_uint = null,

        /// The alignment of the label
        halign: gtk.Align,
        valign: gtk.Align,

        pub var offset: c_int = 0;
    };

    fn init(self: *Self, _: *Class) callconv(.c) void {
        gtk.Widget.initTemplate(self.as(gtk.Widget));

        const priv = self.private();
        if (priv.first_delay > 0) {
            priv.delay_timer = glib.timeoutAdd(
                priv.first_delay,
                onDelayTimer,
                self,
            );
        }
    }

    /// Set the label for the overlay. This will not show the
    /// overlay if it is currently hidden; you must call schedule.
    pub fn setLabel(self: *Self, label: ?[:0]const u8) void {
        const priv = self.private();
        if (priv.label_text) |v| glib.free(@ptrCast(@constCast(v)));
        priv.label_text = null;
        if (label) |v| priv.label_text = glib.ext.dupeZ(u8, v);
        self.as(gobject.Object).notifyByPspec(properties.label.impl.param_spec);
    }

    /// Schedule the overlay to be shown. To avoid flickering during
    /// resizes we schedule the overlay to be shown on the next idle tick.
    pub fn schedule(self: *Self) void {
        const priv = self.private();

        // If we have a delay timer then we're not showing anything
        // yet so do nothing.
        if (priv.delay_timer != null) return;

        // When updating a widget, wait until GTK is "idle", i.e. not in the middle
        // of doing any other updates. Since we are called in the middle of resizing
        // GTK is doing a lot of work rearranging all of the widgets. Not doing this
        // results in a lot of warnings from GTK and _horrible_ flickering of the
        // resize overlay.
        if (priv.idler != null) return;
        priv.idler = glib.idleAdd(onIdle, self);
    }

    fn onIdle(ud: ?*anyopaque) callconv(.c) c_int {
        const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
        const priv = self.private();

        // No matter what our idler is complete with this callback
        priv.idler = null;

        // Cancel our previous show timer no matter what.
        if (priv.timer) |timer| {
            if (glib.Source.remove(timer) == 0) {
                log.warn("unable to remove size overlay timer", .{});
            }
            priv.timer = null;
        }

        // If we have a label to show, show ourselves. If we don't have
        // label text, then hide our label.
        const text = priv.label_text orelse {
            self.as(gtk.Widget).setVisible(0);
            return 0;
        };

        // Set our label and show it.
        priv.label.setLabel(text);
        self.as(gtk.Widget).setVisible(1);

        // Setup the new timer to hide ourselves after the delay.
        priv.timer = glib.timeoutAdd(
            priv.duration,
            onTimer,
            self,
        );

        return 0;
    }

    fn onTimer(ud: ?*anyopaque) callconv(.c) c_int {
        const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
        const priv = self.private();
        priv.timer = null;
        self.as(gtk.Widget).setVisible(0);
        return 0;
    }

    fn onDelayTimer(ud: ?*anyopaque) callconv(.c) c_int {
        const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
        const priv = self.private();
        priv.delay_timer = null;
        return 0;
    }

    //---------------------------------------------------------------
    // Virtual methods

    fn dispose(self: *Self) callconv(.c) void {
        const priv = self.private();
        if (priv.idler) |v| {
            if (glib.Source.remove(v) == 0) {
                log.warn("unable to remove resize overlay idler", .{});
            }
            priv.idler = null;
        }
        if (priv.timer) |v| {
            if (glib.Source.remove(v) == 0) {
                log.warn("unable to remove resize overlay timer", .{});
            }
            priv.timer = null;
        }
        if (priv.delay_timer) |v| {
            if (glib.Source.remove(v) == 0) {
                log.warn("unable to remove resize overlay delay timer", .{});
            }
            priv.delay_timer = null;
        }

        gtk.Widget.disposeTemplate(
            self.as(gtk.Widget),
            getGObjectType(),
        );

        gobject.Object.virtual_methods.dispose.call(
            Class.parent,
            self.as(Parent),
        );
    }

    fn finalize(self: *Self) callconv(.c) void {
        const priv = self.private();
        if (priv.label_text) |v| {
            glib.free(@ptrCast(@constCast(v)));
            priv.label_text = null;
        }

        gobject.Object.virtual_methods.finalize.call(
            Class.parent,
            self.as(Parent),
        );
    }

    const C = Common(Self, Private);
    pub const as = C.as;
    pub const ref = C.ref;
    pub const unref = C.unref;
    const private = C.private;

    pub const Class = extern struct {
        parent_class: Parent.Class,
        var parent: *Parent.Class = undefined;
        pub const Instance = Self;

        fn init(class: *Class) callconv(.c) void {
            gtk.Widget.Class.setTemplateFromResource(
                class.as(gtk.Widget.Class),
                comptime gresource.blueprint(.{
                    .major = 1,
                    .minor = 2,
                    .name = "resize-overlay",
                }),
            );

            // Bindings
            class.bindTemplateChildPrivate("label", .{});

            // Properties
            gobject.ext.registerProperties(class, &.{
                properties.duration.impl,
                properties.label.impl,
                properties.@"first-delay".impl,
                properties.@"overlay-halign".impl,
                properties.@"overlay-valign".impl,
            });

            // Virtual methods
            gobject.Object.virtual_methods.dispose.implement(class, &dispose);
            gobject.Object.virtual_methods.finalize.implement(class, &finalize);
        }

        pub const as = C.Class.as;
        pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
    };
};
